后台工作者在自己的班级处理

Dan*_*iel 5 c# wpf multithreading backgroundworker

好吧,我有以下问题,希望你能帮助我:

我想创建一个带后台工作程序的WPF应用程序,用于更新richtextboxes和其他UI元素.这个后台工作者应该处理一些数据,例如处理文件夹的内容,做一些解析等等.因为我想在Main类之外移动尽可能多的代码,所以我创建了一个类MyProcess.cs,如下所示(事实上,到目前为止,这个类没有多大意义,如果有更多的处理元素,这个问题已经解决了).一般功能应该是:

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

最后一步导致带有注释的InvalidOperationsException,"调用线程无法访问此对象,因为另一个线程拥有它".我读了一下后台工作者类及其功能.所以我认为它与方法中的this.formatedFilenames.Inlines.Add(new Run(...))调用Execute()有关MyProcess.如果我用字符串列表或类似的东西替换Paragraph属性(没有额外的new()调用),我可以通过get方法访问此成员而不会出现任何问题.与我发现的后台工作者相关的所有示例都只返回基本类型或简单类.

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);
    ...
Run Code Online (Sandbox Code Playgroud)

编辑:由于已经询问过:例如,在按钮单击事件上或通过对话框选择文件夹后调用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;
    }
}
Run Code Online (Sandbox Code Playgroud)

nos*_*tio 2

显然,该Paragraph对象(及其子对象)需要线程关联性。也就是说,它不是线程安全的,并且被设计为仅在创建它的同一线程上使用。

RunWorkerAsync据推测,您是从主 UI 线程调用的,这就是worker_RunWorkerCompleted最终调用的地方。Paragraph因此,您可以在完成工作后访问主线程上的实例。但是,它是在后台工作线程上创建的,位于process.Execute. InvalidOperationsException这就是为什么当您从主线程触摸它时会收到异常。

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

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

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

虽然有一些学习曲线async/await,但绝对值得一试。async -await 标签 wiki列出了一些很棒的资源。

要调用上面async的实现Execute,您需要接受“始终异步”规则。通常,这意味着您将从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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

还可以使用事件模式,使代码流更类似于您处理的原始场景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());
Run Code Online (Sandbox Code Playgroud)

在这种情况下,您应该在事件处理程序中显式检查Task对象属性,例如Task.IsCancelledTask.IsFaultedTask.Exception、。Task.ResultExecuteCompleted