【问题标题】:Backgroundworker processing in own class在自己的班级中进行后台工作人员处理
【发布时间】:2014-03-12 21:16:07
【问题描述】:

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

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

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

最后一步会导致 InvalidOperationsException 并带有注释,“调用线程无法访问此对象,因为不同的线程拥有它。”我阅读了一些关于后台工作人员类及其功能的信息。所以我认为这与MyProcessExecute()方法中的this.formatedFilenames.Inlines.Add(new Run(...))调用有关。如果我将 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);
    ...

编辑: 因为这已被问到: 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;
    }
}

【问题讨论】:

  • +1 为有起始代表和第一个问题的人提供如此布局合理的帖子
  • 你在哪里打电话this.worker.RunWorkerAsync(this.folderContent);
  • 出于好奇,您的目标是什么版本的 .NET 框架?如果您可以访问 4.5 的新 async/await 功能,这些操作会变得容易得多
  • 我正在使用 .NET 4.5,调用 RunWorkerAsync,例如在通过对话框选择目录或向上移动到父文件夹(也通过按钮事件)之后...感谢您的所有 cmets,我明天会尝试并报告。

标签: c# wpf multithreading backgroundworker


【解决方案1】:

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

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

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

相反,您应该在 UI 线程上逐个运行后台操作。您可以为此使用DispatcherTimer,或者您可以方便地使用async/await 运行它(针对.NET 4.5 .NET 4.0 和Microsoft.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);
        }
    }
}

async/await 有一些学习曲线,但当然值得一试。 async-await tag wiki 列出了一些很棒的资源。

要像上面那样调用Executeasync 实现,您需要接受"Async all the way" 规则。通常,这意味着您将从顶级事件或命令处理程序(也是 async)调用 Execute,并从其结果调用 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());

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

【讨论】:

  • 谢谢你,最后,我将你的解决方案与 async/await 一起使用,它按预期工作。我认为,后台工作人员是处理数据的一种优雅方式,但正如您所提到的,这对于段落不起作用(很好)。顺便说一句,还可以在类内部创建一个进度状态标志(例如double progress = 0.0;),增加Execute() 方法的for 循环内部的值并通过另一个@987654356 检索该值@ 主程序内部的方法来更新,例如一个进度条。再次:谢谢大家的帮助!
  • @Daniel,没问题。理想情况下,您应该使用 MVVM 模式并在 Execute 之类的任务中更新 ViewModel。减少像 Execute 这样在 UI 线程上并行运行的操作数量也是一个好主意(注意,它们不是同时运行的,而是逐步在 UI 线程上获得一段执行时间)。
【解决方案2】:

您是否尝试过使用调度程序来调用最后一个代码块?

例子:

private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    Action action = () =>
    {
        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);
    };
    Dispatcher.Invoke(DispatcherPriority.Normal, action);
}

更多关于调度员的信息在这里:http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher(v=vs.110).aspx

【讨论】:

  • BGW 的全部要点在于它将完成的事件(以及除DoWork 之外的所有其他事件)编组到 UI 线程。
  • @Servy 是的 - 但仅当从 UI 线程调用时。根据 MSDN 上的 cmets (msdn.microsoft.com/en-us/library/cc221403(v=vs.95).aspx),RunWorkerCompleted 事件是在 UI 线程或调用 RunWorkerAsync 方法的线程上触发的。由于我们不知道从哪个线程调用 RunWorkerAsync() - 使用调度程序不会受到伤害,并且会确保代码在 UI 线程上得到更新。
  • 解决方案 *从 UI 线程调用 RunWorkerAsync,而不是在完成的处理程序中调用。如果您打算这样做,那么首先使用 BGW 是没有意义的。你也可以直接使用Thread
  • 是的,你是对的 - 当你不关心通过同步上下文编组回特定线程时,使用线程或线程池总是会更有效 - 但这是一个更大的代码更改不仅仅是在处理程序中调用 Dispatcher - 并查看它的用途 - OP 永远不会注意到他的应用程序中的差异。您应该将其发布为答案。
  • 鉴于对问题的编辑,很明显 worker_RunWorkerCompleted 方法已经从 UI 线程调用,这根本不会解决任何问题。
猜你喜欢
  • 2015-02-22
  • 1970-01-01
  • 2016-06-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多