c# - Backgroundworker 在自己的类中处理

标签 c# wpf multithreading backgroundworker

那么,我有以下问题,希望你能帮助我:

我想创建一个 WPF 应用程序,其中包含一个用于更新富文本框和其他 UI 元素的后台工作程序。这个后台 worker 应该处理一些数据,例如处理文件夹的内容,进行一些解析等等。由于我想将尽可能多的代码移到 Main 类之外,因此我创建了一个名为 MyProcess.cs 的类正如你在下面看到的(事实上,这个类到目前为止没有多大意义,如果这个问题已经解决,它将填充更多的处理元素)。一般功能应该是:

  1. MainWindow:将创建一个字符串数组(名为 this.folderContent)
  2. MainWindow:后台工作人员开始将此数组作为参数
  3. 主窗口:DoWork()方法将被调用(我知道,这个现在在一个新线程中运行)
  4. MyProcess:根据给定的字符串数组生成一个(到目前为止未格式化的)段落
  5. MainWindow:如果后台 worker 完成,RunWorkerCompleted()方法被调用(在 UI 线程中运行),它应该通过方法的返回参数更新 WPF RichTextBox

这最后一步导致 InvalidOperationsException 并附注“调用线程无法访问此对象,因为另一个线程拥有它。”我阅读了一些关于后台 worker 类及其功能的内容。所以我认为它与 this.formatedFilenames.Inlines.Add(new Run(...)) 有关调用Execute() MyProcess的方法| .如果我用字符串列表或类似的东西替换 Paragraph 属性(没有额外的 new() 调用),我可以通过 get 方法毫无问题地访问这个成员。我发现的所有与后台 worker 相关的示例都只返回基本类型或简单类。

MainWindow.xaml.cs

    public MainWindow()
    {
        InitializeComponent();
        this.process = new MyProcess();
        this.worker = new BackgroundWorker();
        this.worker.DoWork += worker_DoWork;
        this.worker.RunWorkerCompleted += worker_RunWorkerCompleted;
    }

    private void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        this.process.Execute((string[])e.Argument);
        e.Result = this.process.Paragraph();
    }

    private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        this.rtbFolderContent.Document.Blocks.Clear();
        // the next line causes InvalidOperationsException:
        // The calling thread cannot access this object because a different thread owns it.
        this.rtbFolderContent.Document.Blocks.Add((Paragraph)e.Result);
    }

    ...
    // folderContent of type string[]
    this.worker.RunWorkerAsync(this.folderContent);
    ...

编辑:由于有人提出这个问题:RunWorkerAsync 被称为例如在按钮单击事件上或通过对话框选择文件夹之后,因此在 UI 线程中。

MyProcess.cs

class MyProcess
{
    Paragraph formatedFilenames;

    public MyProcess ()
    {
        this.formatedFilenames = new Paragraph();
    }

    public void Execute(string[] folderContent)
    {
        this.formatedFilenames = new Paragraph();
        if (folderContent.Length > 0)
        {
            for (int f = 0; f < folderContent.Length; ++f)
            {
                this.formatedFilenames.Inlines.Add(new Run(folderContent[f] + Environment.NewLine));
                // some dummy waiting time
                Thread.Sleep(500);
            }
        }
    }

    public Paragraph Paragraph()
    {
        return this.formatedFilenames;
    }
}

最佳答案

显然,Paragraph对象(及其子对象)需要线程关联。也就是说,它不是线程安全的,只能在创建它的同一线程上使用。

据推测,您正在调用 RunWorkerAsync来自主 UI 线程,这就是 worker_RunWorkerCompleted 的地方最终被调用。因此,您访问了 Paragraph 的实例工作完成后在主线程上。但是,它是在 process.Execute 内的后台工作线程上创建的.这就是为什么你得到 InvalidOperationsException当您从主线程触摸它时出现异常。

如果以上对问题的理解是正确的,你大概应该放弃BackgroundWorker了。 .使用后台线程运行 for 没有多大意义循环,其唯一目的是通过 Dispatcher.Invoke 将回调编码到 UI 线程.那只会增加额外的开销。

相反,您应该在 UI 线程上逐个运行后台操作。你可以使用 DispatcherTimer 为此,或者您可以使用 async/await 方便地运行它(使用 Microsoft.Bcl.Async 和 VS2012+ 定位 .NET 4.5 .NET 4.0):

public async Task Execute(string[] folderContent, CancellationToken token)
{
    this.formatedFilenames = new Paragraph();
    if (folderContent.Length > 0)
    {
        for (int f = 0; f < folderContent.Length; ++f)
        {
            token.ThrowIfCancellationRequested();

            // yield to the Dispatcher message loop 
            // to keep the UI responsive
            await Dispatcher.Yield(DispatcherPriority.Background);                

            this.formatedFilenames.Inlines.Add(
                new Run(folderContent[f] + Environment.NewLine));

            // don't do this: Thread.Sleep(500);

            // optionally, throttle it;
            // this step may not be necessary as we use Dispatcher.Yield
            await Task.Delay(500, token);
        }
    }
}

关于 async/await 有一些学习曲线,但这当然值得一试。 async-await tag wiki列出一些很棒的资源,作为开始。

调用async实现Execute像上面一样,你需要接受 "Async all the way"规则。通常,这意味着您会调用 Execute来自顶级事件或命令处理程序,它也是 async , 和 await它的结果,例如:

CancellationTokenSource _cts = null;

async void SomeCommand_Executed(object sender, RoutedEventArgs e)
{
    if (_cts != null)
    {
        // request cancellation if already running
        _cts.Cancel();
        _cts = null;
    }
    else
    {
        // start a new operation and await its result
        try
        {
            _cts = new CancellationTokenSource();
            await Execute(this.folderContent, _cts.Token);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }
}

也可以使用事件模式,使代码流更类似于您处理 RunWorkerCompleted 的原始场景。 :

// fire ExecuteCompleted and pass TaskCompletedEventArgs 
class TaskCompletedEventArgs : EventArgs
{
    public TaskCompletedEventArgs(Task task)
    {
        this.Task = task;
    }
    public Task Task { get; private set; }
}

EventHandler<TaskCompletedEventArgs> ExecuteCompleted = (s, e) => { };

CancellationTokenSource _cts = null;

Task _executeTask = null;

// ... 

_cts = new CancellationTokenSource();

_executeTask = DoUIThreadWorkLegacyAsync(_cts.Token);

// don't await here
var continutation = _executeTask.ContinueWith(
    task => this.ExecuteCompleted(this, new TaskCompletedEventArgs(task)),
    _cts.Token,
    TaskContinuationOptions.ExecuteSynchronously,
    TaskScheduler.FromCurrentSynchronizationContext());

在这种情况下,您应该明确检查 Task对象属性,如 Task.IsCancelled , Task.IsFaulted , Task.Exception , Task.Result在你的里面ExecuteCompleted事件处理程序。

关于c# - Backgroundworker 在自己的类中处理,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21763639/

相关文章:

c# - 如何确定 Dbcontexts 的范围(以防止整个应用程序的单例上下文)

c# - .NET 4 中的 "Hello, World!!"生成 3500 个页面错误

java - OpenGL ES - 主线程上的工作太多

c# - Dapper Multiple Query Read 很慢

c# - 使用 EF6 连接到 DB2

c# - 访问扩展器控件的子级

c# - 如何在两个窗口之间的 Wpf 应用程序中保持 session ?

java - Java 中线程中的线程?

multithreading - 使用 Open MP 将多个线程分配给 Xeon Phi 上的单个并行执行

c# - 如何使用 OrmLite 固定大小而不是 null varchar?