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

3
我想知道在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<>类型的属性怎么办?哪些成员(add、remove、clear、indexer)是线程安全的?我应该使用ConcurrentDictionary吗?


请尝试以下链接,该链接提供了一个线程安全的ObservableCollection类型解决方案,可以从任何线程工作,并且可以通过多个UI线程进行绑定:http://www.codeproject.com/Articles/64936/Multithreaded-ObservableImmutableCollection - Anthony
6个回答

8
我的应用有一个后台线程,定期更新Total的值。假设没有其他内容会更新Total,我需要怎么做才能使它线程安全?
对于标量属性,您不需要做任何特殊处理;PropertyChanged事件会自动调度到UI线程。
现在,如果我想使用Dictionary<>或ObservableCollection<>类型的属性来实现类似的功能,该怎么办?哪些成员(add、remove、clear、indexer)是线程安全的?我应该使用ConcurrentDictionary吗?
不,这是线程不安全的。如果您从后台线程更改ObservableCollection的内容,它会崩溃。您需要在UI线程上执行此操作。一种简单的方法是使用在UI线程上引发其事件的集合,如此处所述的集合。
至于Dictionary,它不会在其内容更改时引发通知,因此UI也不会得到通知。

2
PropertyChanged 如何自动调度到 UI 线程?这是如何实现的?我本来期望事件在触发它的上下文中发生。 - Vlad
1
@ThomasLevesque 这对我来说是新闻,你能提供一些相关文档吗? - CodingGorilla
3
@Coding Gorilla:请查看此MS Connect文章。它证实了Thomas的说法,即绑定会自动进行封送处理。 - user128300
@Vlad,如果是那样的话,它会崩溃(除非你使用Dispatcher.Invoke在UI线程上显式执行该操作)。 - Thomas Levesque
1
@bgura 不,使用字符串也可以正常工作。字符串是不可变的引用类型,因此更改字符串只会用新字符串的引用替换原始字符串的引用,这是原子性完成的。 - Thomas Levesque
显示剩余8条评论

2
模型应该像任何代码一样以线程安全的方式编写;你需要确定是否使用锁、并发容器或其他方式来实现。模型只是库代码,(几乎)不应该知道它的功能将被 MVVM 应用程序使用。
然而,VM 必须在 UI 线程中工作。这意味着它们通常不能依赖于来自模型的事件是否来自 UI 线程,因此,如果订阅的事件不在 UI 线程上,则必须将调用传递或将其存储在任务队列中。
因此,VM 是特定方式关注线程安全的地方,比模型更需要关注线程安全。
另外,视图代码通常可以忽略所有线程问题:它会在专用的 UI 线程中获取所有消息/调用/事件/任何内容,并在 UI 线程中进行自己的调用。
针对你的情况,你的代码不是模型而是 VM,对吗?在这种情况下,你必须在 UI 线程中触发你的事件,否则视图会出现问题。

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 中使用锁来解决此问题。

由于你说只有一个线程更新此属性,因此不存在竞争条件的可能性。 这仅在多个线程(或进程)同时更新相同的内容时才会发生。


1

这个问题提供了ObservableCollection的线程封送版本。

如何从后台线程正确更新数据绑定的DataGridView

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

如果一个线程正在更新,而另一个线程正在读取,则存在读取在更新过程中进行的可能性(例如,Int64正在被修改。第一半(32位)已经在一个线程中更新,在第二半更新之前,该值从第二个线程中读取。读取到完全错误的值)

这可能是一个问题,也可能不是,这取决于您的应用程序将要执行的操作。如果错误的值将在GUI上闪烁1秒钟,那么它可能并不重要,并且可以忽略锁的性能惩罚。如果您的程序将根据该值采取行动,则可能需要锁定它。


0
有些事情比它实际上简单得多...当在视图中实例化您的视图模型时,只需在构造函数中传递调度程序即可。
    ServerOperationViewmodel ViewModel;        
    public pgeServerOperations()
    {
        InitializeComponent();
        ViewModel = new ServerOperationViewmodel(Dispatcher);
    }

然后在你的视图模型中:

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

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

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

我承认我对MVVM还比较新,但我不认为这违反了MVVM的座右铭。


0
一个简单的答案是,你需要通过UI线程的调度器在UI线程中安排属性更新。这会将更新操作放入队列中,从而不会导致应用程序崩溃。
private void handler(object sender, EventArgs e)
{
    Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate { updates(); });
}

private void updates() { /* real updates go here */ }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接