BackgroundWorker RunWorkerCompleted 未在 UI 线程上执行

Posted

技术标签:

【中文标题】BackgroundWorker RunWorkerCompleted 未在 UI 线程上执行【英文标题】:BackgroundWorker RunWorkerCompleted not executed on UI thread 【发布时间】:2020-01-08 12:04:30 【问题描述】:

我已经为 BackgroundWorker 写了一个小包装器:

public class Worker

  public void Execute<T>(Func<T> work, Action<Exception, T> workCompleted)
  
    var worker = new BackgroundWorker();
    worker.DoWork += (o, ea) =>  ea.Result = work(); ;
    worker.RunWorkerCompleted += (o, ea) =>  workCompleted(ea.Error, (T)ea.Result); ;
    worker.RunWorkerAsync();
  

我在视图模型(在 XAML 中创建)中使用它,如下所示:

public class MainWindowViewmodel : INotifyPropertyChanged

  private ObservableCollection<Job> _jobs = new ObservableCollection<Job>();
  public ICollectionView JobsView  get;  = CollectionViewSource.GetDefaultView(_jobs);
  public RelayCommand RefreshJobsCommand => new RelayCommand(x => UpdateJobs()); /*edit1*/

  private void UpdateJobs() /*executed via command binding*/
  
    new Worker().Execute(() => _webServiceManager.JobService.GetJobsByDate(settings.Limit, DateTime.Now.AddDays(-365)), (ex, jobArray) =>
      
        jobArray.toList().ForEach(j=>_jobs.Add(j));
      );
  

workCompleted 中的作业添加到 ObservableCollection _jobs 时,我得到以下异常: This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.

所以显然RunWorkerCompleted 回调不会在 UI 线程上运行。但据我所知,它应该在调用 RunWorkerAsync 的一个线程上运行?这应该是 UI 线程,因为它是从命令绑定(也是 UI 线程)的视图模型(从 XAML 创建)内部调用的,对吧?

我在这里错过了什么?

编辑2: 我可以将问题缩小到作业的初始加载。这是由MainWindow 的ctor 完成的,将导致所描述的问题。之后通过命令绑定使用 UI 按钮进行的​​任何其他更新都可以正常工作。

所以问题是,在初始加载时,MainWindowctor、ViewModelctor 和 Worker.Execute() 在 UI 线程上执行,但 workCompleted 不是,尽管 SynchronizationContext 应该设置为 UI调用RunWorkerAsync时的线程。

每当通过命令绑定触发下一次加载时,workCompleted 也会设置为 UI 线程的 SynchronizationContext

知道为什么这首先不起作用吗?

public partial class MainWindow : Window

   private MainWindowViewmodel _viewmodel;

   public MainWindow(Connection connection)
   
      InitializeComponent();
      Debug.WriteLine("mainwindow-ctor: " + Thread.CurrentThread.ManagedThreadId.ToString());
      _viewmodel = (MainWindowViewmodel)DataContext;
      _viewmodel.UpdateJobs();
   

【问题讨论】:

不要使用 BackgroundWorker。使用任务和异步/等待 GetJobsByDate() 是做什么的?它如何确保你传递给它的 lambda 在 ui 线程上执行?我的意思是_webServiceManager 听起来并不同步。 你真的在 UI 线程上调用 UpdateJobs() 吗?请提供一个MCVE来证明您的问题。 external 组件 _webServiceManager 是同步实现的,所以我必须将所有调用包装到一个 Task.Run() 中,恕我直言不太好。跨度> @mm8 我为 UI 按钮绑定的命令添加了代码。这将触发执行UpdateJobs(),它仍然应该是 UI 线程...? 【参考方案1】:

处理RunWokerCompleted 事件的线程由调用RunWorkerAsync 方法时的SynchronizationContext.Current 属性确定。

因此,如果您的 Execute 方法实际上是在 WPF 应用程序中的 UI 线程上调用的,其中 SynchronizationContext.Current 属性返回一个 DispatcherSynchronizationContextworkCompleted 也将在此线程上调用。您可以通过在MainWindow 的构造函数中调用Execute() 自己轻松地确认这一点:

public partial class MainWindow : Window

    public MainWindow()
    
        InitializeComponent();

        new Worker().Execute(() =>  Thread.Sleep(5000); return 0; , (ex, jobArray) =>
        
            MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString());
        );
    

顺便说一句,自 .NET Framework 4 以来,Task Parallel Library (TPL) 是编写多线程和并行代码的首选方式。任务异步编程模型 (TAP) 以及在 . NET 4.5 使其更易于使用。

【讨论】:

我添加了一些线程 ID 的输出,如您的示例所示。有趣的是:mainwindow ctor、viewmodel ctor、Worker.Execute() => 都具有相同的线程 id。所以RunWorkerAsync 是从这个确切的线程中调用的。仅在workComplete委托中,线程id不同 @Matthias:你检查SynchronizationContext.Current的值了吗? 我将SynchronizationContext.Current 与之前相同位置的equals 进行了比较,结果是相同的。 mainWindow、viewModel 和 Worker.Execute() 中的上下文相同,但 workCompletedelegate 中的上下文不同【参考方案2】:

通过将作业的初始加载从 MainWindow 构造函数移至 MainWindow.Loaded 事件来解决问题。

由于某种原因,除非 UI 完全初始化,否则 BackgroundWorker 在 UI 线程上没有 SynchronizationContext RunWorkerCompleted

public partial class MainWindow : Window

   private MainWindowViewmodel _viewmodel;

   public MainWindow(Connection connection)
   
      InitializeComponent();
      _viewmodel = (MainWindowViewmodel)DataContext;
      Loaded += MainWindowLoaded;
   

   private void MainWindowLoaded(object sender, RoutedEventArgs e)
   
      _viewmodel.UpdateJobs();
   

【讨论】:

以上是关于BackgroundWorker RunWorkerCompleted 未在 UI 线程上执行的主要内容,如果未能解决你的问题,请参考以下文章

BackgroundWorker控件使用

C# BackGroundWorker backgroundWorker1_DoWork中,按钮不能按的问题

将 'BackgroundWorker' 替换为 'Thread'

winform BackgroundWorker的使用

简单多线程BackgroundWorker

backgroundWorker1