C#线程安全(特别是MVVM / WPF)

时间:2012-06-13 13:13:58

标签: c# wpf mvvm

我想知道在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吗?

6 个答案:

答案 0 :(得分:8)

  

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

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

  

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

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

至于Dictionary<TKey, TValue>,它的内容发生变化时不会发出通知,因此无论如何都不会通知用户界面。

答案 1 :(得分:2)

假设有两个线程正在更新Total,并且您希望在_total方法中将所有更改记录到PropertyChanged。现在存在PropertyChanged可能错过值的竞争条件。当线程在调用set_Total的过程中阻塞时会发生这种情况。它会更新_total,但仍会调用PropertyChanged。在此期间,另一个线程将_total更新为另一个值:

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

现在,永远不会使用值4来调用PropertyChanged

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

由于您说您有一个更新此属性的线程,因此不存在竞争条件。只有当多个线程(或进程)同时更新同一个东西时才会这样。

答案 2 :(得分:1)

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

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

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

反过来,View代码通常可以对所有线程问题感到高​​兴:它在专用UI线程中获取所有消息/调用/事件/任何内容,并在UI线程中进行自己的调用


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

答案 3 :(得分:1)

这个问题提供了ObservableCollection

的线程编组版本

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

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

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

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

答案 4 :(得分:0)

一个简单的答案是您需要通过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 */ }

答案 5 :(得分:0)

有些事情更详细,它实际上是如此简单......当你在视图中实例化你的viewmodel时,只需将调度程序传递给ctor。

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

然后在您的View模型中:

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

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

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

我承认我仍然是MVVM的新手,但我不认为这打破了MVVM的座右铭。