【问题标题】:What is the best way of ensuring that an async method can update a non-threadsafe collection?确保异步方法可以更新非线程安全集合的最佳方法是什么?
【发布时间】:2013-11-26 16:43:36
【问题描述】:

我正在进行异步调用,最终将更新 GUI 中的集合。异步调用是通过这样的委托命令完成的:

StartDoingUsefulStuffCommand = new DelegateCommand(() => Task.Run(() => StartDoingUsefulStuff()));

public async Task StartDoingUsefulStuff()
{
    try
    {
        await some method
        do something else
        MyCollection.Clear();
        ...
        ...
    }
    catch (Exception e)
    {
       // handle exception
    }
}

最终 StartDoingUsefulStuff 方法想要更新一个集合,导致这个异常,因为另一个线程试图更新一个集合:

“System.NotSupportedException”类型的第一次机会异常 发生在 PresentationFramework.dll 中

补充信息:这种类型的 CollectionView 不支持 从不同于 调度线程。

我找到了一种解决方案:在当前 SynchronizationContext 中运行任务:

Task.Factory.StartNew(() => StartDoingUsefulStuff(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());

这可以正常工作,但对我来说,每次从 XAML 调用命令时执行所有这些工作似乎很乏味。您对我目前的解决方案有何看法/最佳做法是什么?

【问题讨论】:

  • 所以你会推荐调用 Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => MyCollection.Clear())); ?
  • 我想说的是,如果您想从不同的线程设置 UI 值,这可能是一个选项。而这一切都取决于你如何使用它。
  • 看起来你正在修改MyCollection,而它正在被循环使用?
  • @rhughes 不一定,在这种情况下也不是

标签: c# wpf async-await


【解决方案1】:

还有一种更简单的修复方法。在初始化集合的构造函数中,只需在初始化后添加以下行。

BindingOperations.EnableCollectionSynchronization(MyCollection,_lock); 其中_lockprivate static object。那时您不必再关心命令中的线程安全了。然后您就可以安全地使用await Task.Run了。

【讨论】:

    【解决方案2】:

    你已经给出了正确的答案。这里的问题不在于集合不是线程安全的。问题是集合不能被UI线程以外的线程修改。

    这是 UI 控件的常见“问题”,无论是 WPF 还是 WinForms。

    解决方案确实是传递在“当前同步上下文”中运行任务所需的选项,这意味着(在 WPF 中)完成在调用它们的线程中运行,通常是 UI 线程。

    您不必一直这样做。事实上,当您手动启动新任务时,您只需执行一次。默认情况下,C# 5 的“异步”行为也是这样做的,这就是为什么我们大多数人都有做相反的繁琐任务的原因:告诉调度程序不要使用以下代码使用相同的同步上下文:

    var result = await SomeAsync().ConfigureAwait(false);
    

    使用默认行为存在问题,包括锁定主线程,但在您的情况下,这正是您想要的。

    也许您应该考虑不调用 Task.Run(),而是直接调用 async 方法(不使用 Task.Run())并丢弃返回的任务对象(即即发即弃)。

    【讨论】:

    • 我喜欢你的解释。但是,对于您的最后一条语句(不调用 task.run),如果使用 async void 声明该方法,则单元测试将随机失败。因此,为了避免这种情况,我开始将异步任务用于“回调方法”
    • 是的,单元测试运行器可能不支持异步,我自己也遇到过这个问题。这并不意味着您需要异步才能调用异步函数。一个方法只有在它想要执行等待操作时才需要是异步的。
    【解决方案3】:

    使用“MyCollection”的调度器对象 像这样的:

    MyCollection.Dispatcher.BeginInvoke((Action)(() => 
    { 
        MyCollection.Clear()) 
    }));
    

    【讨论】:

      【解决方案4】:

      .NET 中的某些对象具有线程关联性,通常与创建它们的线程相关。这就是您收到 NotSupportedException 的原因,因为您试图从错误的线程更改具有线程关联性的对象。

      基本上,您需要确保任何可能因线程关联而遇到问题的操作都在正确的线程上执行。

      您可以利用Dispatcher 对象轻松地在特定线程上执行任意代码。如果您使用 ViewModels,您可以要求它们被注入 Dispatcher(即作为构造函数参数),表示创建具有线程关联的对象的 Dispatcher。

      public abstract class ViewModelBase
      {
          public ViewModelBase(Dispatcher dispatcher)
          {
              UIDispatcher = dispatcher;
          }
      
          protected Dispatcher UIDispatcher;
      }
      

      当您需要执行一段必须在 UI 线程上发生的代码时,您可以利用 Dispatcher,并酌情使用 InvokeBeginInvoke 方法。

      保留 Dispatcher 似乎也是 Microsoft 处理具有线程关联性的对象的方式。您正在使用的 CollectionView 对象有一个 Dispatcher 属性(继承自 DispatcherObject),您可以使用该属性在适当的线程上执行代码。

      MyCollection.Dispatcher.Invoke(new Action(() => MyCollection.Clear()));
      

      我建议不要使用 TaskScheduler.FromCurrentSynchronizationContext(),除非您绝对确定该操作不需要任何有意义的处理时间。您希望尽可能关闭 UI 线程的操作,以保持响应式 UI。使用 Tasks 是一个很好的方法,但使用从 UI 线程创建的 TaskScheduler 绝对是解决线程关联问题的错误方法,假设维护响应式 UI 很重要。

      【讨论】:

      • 能否请您详细说明“您可以要求为他们注入 Dispatcher”以及您打算如何做?
      • @emedbo 通常,您可以完全控制 ViewModel 的创建,因此您需要一个有效的 Dispatcher 对象(即作为构造函数参数),然后在创建时对其进行排序。在顶层(或创建 ViewModel 的任何位置),您可以识别 UI 线程,然后确保在创建 ViewModel 时将其注入。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-07
      • 1970-01-01
      • 2021-11-06
      • 1970-01-01
      • 2020-01-15
      相关资源
      最近更新 更多