【问题标题】:Why is CollectionChanged not threadsafe?为什么 CollectionChanged 不是线程安全的?
【发布时间】:2013-10-11 14:33:17
【问题描述】:

我正在开发一个 WPF 应用程序,发现绑定属性的属性更改通知可以从后台线程发生,但是对于 observablecollection 的任何更改(如添加或删除项目)必须从 UI 线程发生。我的问题是为什么会这样? INotifyPropertyChanged 和 INotifyCollectionChanged 都是由 UI 控件订阅的,那为什么 INotifyPropertyChanged 会例外?

例如:

 public class ViewModel : INotifyPropertyChanged
    {
        ObservableCollection<Item> _items = new ObservableCollection<Item>();

        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                //Can fire this from a background thread without any crash and my 
                //Name gets updated in the UI
                InvokePropertyChanged(new PropertyChangedEventArgs("Name"));
            }
        }

        public void Add(Item item)
        {
            //Cant do this from a background thread and has to marshal.
            _items.Add(item);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void InvokePropertyChanged(PropertyChangedEventArgs e)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, e);
        }
    }

注意:来自后台线程的 CollectionChanged 事件使应用程序崩溃,但是来自后台线程的 PropertyChanged 事件更新 UI 没有任何问题,是的,这是在 .NET 4.0 中

【问题讨论】:

  • ObservableCollection 不是线程安全的类,很少有集合类是。不需要在 UI 线程上进行任何更改。但是,如果您在修改 UI 的类中侦听 PropertyChanged 事件,那么它肯定会开始变得重要。这与集合类没有任何关系,与您的事件处理程序代码所做的一切有关。
  • @Hans:确切地说,通知属性已更改和通知集合已更改均由 UI 控件(如网格)订阅。为什么它允许从后台线程(无需编组)引发标量属性的属性更改通知,但 NotifyCollectionChanged 只能从 UI 线程?

标签: c# wpf multithreading


【解决方案1】:

这个问题与线程安全无关。问题是CollectionChanged 事件是从工作线程引发的,这意味着处理程序在同一个线程中执行,并且当处理程序尝试触摸 UI 时,您会遇到异常,因为只有 UI 线程才允许这样做。

如果情况相同,PropertyChanged 事件也会发生同样的情况,对任何一个事件都没有特殊处理。

如果您需要从事件处理程序中触摸 UI,那么您必须确保 the event is raised on the UI thread 或者如果需要对 UI 线程进行编组更改,则事件处理程序必须检查 Dispatcher.CheckAccessDispatcher.BeginInvoke这样做。

【讨论】:

  • 从后台线程提高 Collection Changed 会使应用程序崩溃,但 PropertyChanged 不会。当我引发 PropertyChanged 事件时,我会想象处理程序仍在同一个线程上下文中执行(或者框架是否为您执行此操作(?),如果是,为什么不更改集合?)
  • @Mike:这完全取决于事件处理程序的作用。它们总是在引发事件的线程上执行,除非有明确的事情发生,否则在这种情况下没有。
  • your 处理程序正在为事件执行吗?如果是这样,很可能您的事件处理没有考虑到事件可能不在预期线程上的事实。
  • UpdateUI-更新显示在 UI 上,没有任何崩溃。最初的问题是,为什么绑定管道对标量属性而不是集合这样做?
  • @Mike:我不确定是否有人可以在 WPF 团队之外回答这个问题。冒昧地猜测一下,由属性触发的更改是“简单”的操作:毕竟,管道有一个值,它想要推送到控件,并且无论发生什么事情,它都将是安全的工作线程。虽然如果一个集合发生了变化,那么我们将面临更多的工作(创建新窗口、确定它们的绑定值等),并且如果工作线程仍在修改内容,则开始采取行动将引发竞争条件。
【解决方案2】:

您很可能是指绑定机制。

绑定不能直接与 CLR 属性和 PropertyChanged 事件一起使用。
Binding 在其工作中使用反射。 它们按优先级顺序排列:PropertyDescriptor、PropertyInfo、DependencyProperty 和 DynamicPropertyAccessor。
你可以在这里看到它:PropertyPathWorker.SetValue(object item, object value).
出于同样的原因,如果一个属性仅通过绑定进行更改,那么无论是否存在 INotifyPropertyChanged 接口,都会对其进行监视。
此外,使用反射可以让您不必担心观察到的属性会在哪个流中发生变化。

在集合的情况下,绑定也对该集合将分配给受监视属性的流不敏感。
BUT 对集合中的更改(INotifyCollectionChanged 和 IBindingList)的监控不再由绑定机制提供,而是由 ItemsControl 类的内部逻辑提供。
已经有对观察事件的直接订阅,这使得该逻辑对集合将更改的线程敏感。 如果不是UI线程,那么观察会被销毁,甚至会抛出异常。

相信这一点就足够了。

演示示例。 具有随机集合变化的类 ViewModel。

public class RandomCollectionViewModel
{
    public ObservableCollection<int> Numbers { get; }
        = new ObservableCollection<int>() { 1, 2, 3 };
    private static readonly Random random = new Random();
    private readonly Timer timer = new Timer();

    public RandomCollectionViewModel()
    {
        timer.Interval = 500;
        timer.Elapsed += (s, e)
            => Numbers[random.Next(Numbers.Count)] = random.Next();
        timer.Start();
    }
}

XAML 使用绑定来显示集合项:

<StackPanel>
    <FrameworkElement.DataContext>
        <local:RandomCollectionViewModel/>
    </FrameworkElement.DataContext>
    <TextBlock Text="{Binding Numbers[0]}"/>
    <TextBlock Text="{Binding Numbers[1]}"/>
    <TextBlock Text="{Binding Numbers[2]}"/>
</StackPanel>

XAML 使用ItemsControl 显示集合项:

<StackPanel>
    <FrameworkElement.DataContext>
        <local:RandomCollectionViewModel/>
    </FrameworkElement.DataContext>
    <ItemsControl ItemsSource="{Binding Numbers}"/>
</StackPanel>

这些示例清楚地显示了绑定和ItemsContol 的工作差异。

【讨论】:

    猜你喜欢
    • 2012-11-20
    • 2020-10-10
    • 1970-01-01
    • 2016-08-14
    • 2023-03-12
    • 1970-01-01
    • 2023-03-30
    • 2012-03-22
    • 2015-07-24
    相关资源
    最近更新 更多