【问题标题】:C# thread safety (in particular MVVM/WPF)C# 线程安全(特别是 MVVM/WPF)
【发布时间】:2012-06-13 13:13:58
【问题描述】:

我想知道我需要做些什么才能使模型在 MVVM 中线程安全。假设我有以下类,它被实例化为单例:

public class RunningTotal: INotifyPropertyChange
{
   private int _total;
   public int Total
   {
      get { return _total; }
      set
      {
         _total = value;
         PropertyChanged("Total");
      }
   }
   ...etc...
}

我的视图模型通过属性公开它:

public RunningTotal RunningTotal { get; }

我的视图绑定了一个文本块,即{Binding Path=RunningTotal.Total}

我的应用有一个后台线程,它会定期更新 Total 的值。假设没有其他东西更新 Total,我应该做什么(如果有的话)使所有这些线程安全?

现在,如果我想做类似的事情但使用Dictionary<>ObservableCollection<> 类型的属性怎么办?哪些成员(添加、删除、清除、索引器)是线程安全的?我应该改用 ConcurrentDictionary 吗?

【问题讨论】:

标签: c# wpf mvvm


【解决方案1】:

我的应用有一个后台线程,它会定期更新 Total 的值。假设没有其他东西更新 Total,我应该做什么(如果有的话)使所有这些线程安全?

对于标量属性,你不需要做任何特别的事情; PropertyChanged 事件会自动封送至 UI 线程。

现在,如果我想做类似的事情但使用 Dictionary 或 ObservableCollection 类型的属性怎么办?哪些成员(添加、删除、清除、索引器)是线程安全的?我应该改用 ConcurrentDictionary 吗?

不,这不是线程安全的。如果您从后台线程更改ObservableCollection<T> 的内容,它将中断。您需要在 UI 线程上执行此操作。一种简单的方法是使用一个在 UI 线程上引发其事件的集合,就像 here 中描述的那样。

至于Dictionary<TKey, TValue>,它不会在其内容发生变化时发出通知,因此无论如何也不会通知UI。

【讨论】:

  • PropertyChanged 是如何自动编组到 UI 线程的?魔法在哪里?我希望事件发生在它被触发的上下文中。
  • @Vlad,“魔法”在绑定引擎中。 PropertyChanged 事件本身可能会在后台线程上触发,但绑定引擎会在 UI 线程上对其做出反应(使用 Dispatcher.Invoke)
  • @ThomasLevesque 这对我来说是个新闻,你能提供一些相关文件吗?
  • @Coding Gorilla:见MS Connect article。它证实了 Thomas 声称绑定是自动编组的。
  • @bgura 不,它可以与字符串一起正常工作。 String 是不可变的引用类型,因此更改字符串只是将对原始字符串的引用替换为对新字符串的引用,这是原子完成的。
【解决方案2】:

假设有两个线程更新Total,并且您想在PropertyChanged 方法中记录对_total 的所有更改。现在有一个竞争条件,PropertyChanged 可能会丢失一个值。当线程在调用set_Total 的过程中阻塞时会发生这种情况。它更新了_total,但还调用了PropertyChanged。与此同时,另一个线程将_total 更新为另一个值:

thread1: _total = 4;
thread2: _total = 5;
thread2: PropertyChanged("Total");
thread1: PropertyChanged("Total");

现在,PropertyChanged 永远不会以 4 的值调用。

您可以通过将值传递给PropertyChanged 方法或在setter 中使用锁来解决此问题。

由于您说您有一个更新此属性的线程,因此不可能出现竞争条件。只有当多个线程(或进程)同时更新同一事物时才会出现这种情况。

【讨论】:

    【解决方案3】:

    模型应该像任何代码一样以线程安全的方式编写;由您决定是否使用锁、并发容器或其他任何方式来执行此操作。模型只是库代码,它(几乎)不应该知道它的功能将被 MVVM 应用程序使用。

    但是,VM 必须在 UI 线程中工作。这意味着他们通常不能依赖模型中的事件来自 UI 线程,因此如果订阅的事件不在 UI 线程中,他们必须编组调用或将它们存储在任务队列中。

    因此,VM 应该以特定方式关心线程安全,而不是模型需要。

    反过来,View 代码通常可以愉快地忽略所有线程问题:它在专用 UI​​ 线程中获取所有消息/调用/事件/任何内容,并在 UI 线程中进行自己的调用.


    特别是对于您的情况,您的代码不是模型而是 VM,对吗?在这种情况下,您必须在 UI 线程中触发事件,否则 View 会不高兴。

    【讨论】:

      【解决方案4】:

      这个问题提供了 ObservableCollection 的线程编组版本

      How do you correctly update a databound datagridview from a background thread

      但是,您仍然需要担心线程之间的争用,这将需要您在资源更新时锁定资源,或者使用诸如 Interlocked.Increment 之类的东西。

      如果一个线程正在更新,而另一个线程正在读取,则存在读取在更新的一半时完成的可能性(例如,正在修改 Int64。前半部分(32 位)已在一个中更新线程,并且在更新后半部分之前,从第二个线程读取值。读取的值完全错误)

      这可能是也可能不是问题,具体取决于您的应用程序将要执行的操作。如果错误的值会在 GUI 上闪烁 1 秒,那么它可能没什么大不了的,锁的性能损失可以忽略不计。如果您的程序要根据该值采取行动,那么您可能需要将其锁定。

      【讨论】:

        【解决方案5】:

        一个简单的答案是,您需要通过 UI Thread 的 Dispatcher 来安排 UI Thread 中的属性更新。这会将更新操作放入不会使应用程序崩溃的队列中。

        private void handler(object sender, EventArgs e)
        {
            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate { updates(); });
        }
        
        private void updates() { /* real updates go here */ }
        

        【讨论】:

          【解决方案6】:

          一些更复杂的事情实际上是如此简单......当在您的视图中实例化您的视图模型时,只需将调度程序向下传递到 ctor 中。

              ServerOperationViewmodel ViewModel;        
              public pgeServerOperations()
              {
                  InitializeComponent();
                  ViewModel = new ServerOperationViewmodel(Dispatcher);
              }
          

          然后在您的视图模型中:

          Dispatcher UIDispatcher;
          public ServerOperationViewmodel(Dispatcher uiDisp)
          {
              UIDispatcher = uiDisp;
          }
          

          并像使用普通 UI 调度程序一样使用它。

          UIDispatcher.Invoke(() =>
          {
            .......
          });
          

          我承认我对 MVVM 还是很陌生,但我不认为这违反了 MVVM 的座右铭。

          【讨论】:

            猜你喜欢
            • 2010-12-14
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2010-12-04
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多