如何通过工作线程更新ObservableCollection?

Mac*_*iek 71 c# wpf multithreading observablecollection

我有一个ObservableCollection<A> a_collection;该集合包含'n'个项目.每个项目A看起来像这样:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}
Run Code Online (Sandbox Code Playgroud)

基本上,它都连接到WPF列表视图+详细信息视图控件,它在单独的列表视图中显示所选项目的b_subcollection(双向绑定,propertychanged上的更新等).当我开始实现线程时,问题出现了.整个想法是让整个a_collection使用它的工作线程来"工作",然后更新它们各自的b_subcollections并让gui实时显示结果.

当我尝试它时,我得到一个例外,说只有Dispatcher线程可以修改ObservableCollection,并且工作停止了.

任何人都可以解释这个问题,以及如何解决它?

干杯

Jon*_*Jon 113

.NET 4.5的新选项

从.NET 4.5开始,有一个内置机制可以自动同步对集合的访问并将CollectionChanged事件分派给UI线程.要启用此功能,您需要从UI线程中进行调用.BindingOperations.EnableCollectionSynchronization

EnableCollectionSynchronization 做两件事:

  1. 记住调用它的线程,并使数据绑定管道编组CollectionChanged该线程上的事件.
  2. 获取对集合的锁定,直到处理了编组事件,以便运行UI线程的事件处理程序在从后台线程修改集合时不会尝试读取集合.

非常重要的是,这并不能解决所有问题:为了确保对本质上不是线程安全的集合的线程安全访问,您必须通过在即将修改集合时从后台线程获取相同的锁与框架协作.

因此,正确操作所需的步骤是:

1.确定您将使用何种锁定

这将决定EnableCollectionSynchronization必须使用哪个过载.大多数情况下,一个简单的lock语句就足够了,所以这个重载是标准选择,但是如果你使用一些花哨的同步机制,那么也支持自定义锁.

2.创建集合并启用同步

根据所选的锁定机制,在UI线程上调用适当的重载.如果使用标准lock语句,则需要提供锁定对象作为参数.如果使用自定义同步,则需要提供CollectionSynchronizationCallback委托和上下文对象(可以是null).调用时,此委托必须获取您的自定义锁,调用Action传递给它并在返回之前释放锁.

3.在修改之前锁定集合进行协作

当您要自己修改集合时,还必须使用相同的机制锁定集合; 使用lock()EnableCollectionSynchronization简单方案中传递给的同一个锁定对象,或在自定义方案中使用相同的自定义同步机制执行此操作.

  • 我在哪里可以找到一个有效的例子? (11认同)
  • 一个小例子可以使这个答案更有用.我认为这可能是正确的解决方案,但我不知道如何实现它. (8认同)
  • @Kohanz 调用 UI 线程调度程序有很多缺点。最大的问题是,在 UI 线程实际处理分派之前,您的集合不会更新,然后您将在 UI 线程上运行,这可能会导致响应问题。另一方面,使用锁定方法,您可以立即更新集合并可以继续在后台线程上进行处理,而无需依赖 UI 线程执行任何操作。UI 线程将根据需要赶上下一个渲染周期的更改。 (4认同)
  • 从这个关于 EnableCollectionSynchronization 的线程的答案中有更多的见解:/sf/answers/1155821831/ (3认同)
  • 这是否会导致集合更新被阻塞,直到UI线程能够处理它们为止?在涉及到不可变对象的单向数据绑定集合的场景中(一种相对常见的场景),似乎有可能具有一个集合类,该类将保留每个对象的“最后显示版本”以及更改队列。 ,并使用`BeginInvoke`运行一种方法,该方法将在UI线程中执行所有适当的更改(最多任何一个`BeginInvoke`在任何给定时间都将处于挂起状态。 (2认同)
  • 我一直在研究 4.5 中的集合同步大约一个月了,我认为这个答案有些不正确。答案指出启用调用必须发生在 UI 线程上,而回调发生在 UI 线程上。这两种情况似乎都不是。我能够在后台线程上启用集合同步,并且仍然使用此机制。此外,框架中的深度调用不进行任何编组(参见 ViewManager.AccessCollection.https://referencesource.microsoft.com/#PresentationFramework/src/Framework/MS/Internal/Data/ViewManager.cs) (2认同)

Jos*_*osh 67

从技术上讲,问题不在于您是从后台线程更新ObservableCollection.问题是当你这样做时,集合会在导致更改的同一个线程上引发其CollectionChanged事件 - 这意味着控件正在从后台线程更新.

为了在控件绑定到后台线程时从后台线程填充集合,您可能必须从头开始创建自己的集合类型以解决此问题.有一个更简单的选项可能会对你有用.

将添加调用发布到UI线程.

public static void AddOnUI<T>(this ICollection<T> collection, T item) {
    Action<T> addMethod = collection.Add;
    Application.Current.Dispatcher.BeginInvoke( addMethod, item );
}

...

b_subcollection.AddOnUI(new B());
Run Code Online (Sandbox Code Playgroud)

此方法将立即返回(在项目实际添加到集合之前),然后在UI线程上,该项目将添加到集合中,每个人都应该感到高兴.

然而,事实是,由于所有的跨线程活动,此解决方案可能会在重负载下陷入困境.一个更有效的解决方案是批量处理一堆项目并定期将它们发布到UI线程,这样您就不会跨每个项目的线程调用.

BackgroundWorker的类实现的模式,使您可以通过汇报其进展ReportProgress后台操作过程中的方法.通过ProgressChanged事件在UI线程上报告进度.这可能是您的另一种选择.

  • 这个答案很简单.谢谢你的分享! (5认同)

Whi*_*eep 16

使用.NET 4.0,您可以使用这些单行:

.Add

Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));
Run Code Online (Sandbox Code Playgroud)

.Remove

Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
Run Code Online (Sandbox Code Playgroud)


Lad*_*gic 5

后代的集合同步代码。这使用简单的锁定机制来启用集合同步。请注意,您必须在UI线程上启用集合同步。

public class MainVm
{
    private ObservableCollection<MiniVm> _collectionOfObjects;
    private readonly object _collectionOfObjectsSync = new object();

    public MainVm()
    {

        _collectionOfObjects = new ObservableCollection<MiniVm>();
        // Collection Sync should be enabled from the UI thread. Rest of the collection access can be done on any thread
        Application.Current.Dispatcher.BeginInvoke(new Action(() => 
        { BindingOperations.EnableCollectionSynchronization(_collectionOfObjects, _collectionOfObjectsSync); }));
    }

    /// <summary>
    /// A different thread can access the collection through this method
    /// </summary>
    /// <param name="newMiniVm">The new mini vm to add to observable collection</param>
    private void AddMiniVm(MiniVm newMiniVm)
    {
        lock (_collectionOfObjectsSync)
        {
            _collectionOfObjects.Insert(0, newMiniVm);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)