如何通过辅助线程更新ObservableCollection?


83

我有一个ObservableCollection<A> a_collection;集合包含“ n”个项目。每个项目A如下所示:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

基本上,所有这些都连接到WPF列表视图+一个详细信息视图控件,该控件b_subcollection在单独的列表视图中显示所选项目的内容(2向绑定,属性更改的更新等)。

当我开始实施线程时,问题就出现了。整个想法是让a_collection工作线程充分利用它来“完成工作”,然后更新各自的工作,b_subcollections并让gui实时显示结果。

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

谁能解释这个问题,以及如何解决?


尝试以下提供了线程安全解决方案的链接,该解决方案可以在任何线程中使用,并且可以通过多个UI线程进行绑定:codeproject.com/Articles/64936/…–
Anthony

Answers:


74

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

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

将Add调用发布到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());

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

但是现实情况是,由于所有跨线程活动,该解决方案很可能在高负载下陷入困境。一个更有效的解决方案将批处理一堆项目,并将它们定期发布到UI线程中,这样您就不会在每个项目的线程间进行调用。

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


BackgroundWorker的runWorkerAsyncCompleted怎么样?那也绑定到UI线程吗?
Maciek'1

1
是的,BackgroundWorker的设计方式是使用SynchronizationContext.Current引发其完成和进度事件。DoWork事件将在后台线程上运行。下面是关于在WPF线程一个很好的文章,讨论的BackgroundWorker太msdn.microsoft.com/en-us/magazine/cc163328.aspx#S4
乔什-

5
这个答案很简单。感谢分享!
烧杯

@Michael在大多数情况下,后台线程不应阻塞并等待UI更新。如果两个线程彼此等待,并且使用Dispatcher.Invoke,则存在死锁的风险,充其量只能使代码的性能大打折扣。在您的特殊情况下,您可能需要这样做,但是在绝大多数情况下,您的最后一句话根本不正确。
乔什

@Josh我删除了我的答案,因为我的情况似乎很特殊。我将在设计中进一步研究,然后再三考虑,可以做得更好的方法。
迈克尔

125

.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在简单的场景,或在自定义场景相同的自定义同步机制。


2
这会导致集合更新被阻止,直到UI线程能够处理它们为止?在涉及到不可变对象的单向数据绑定集合的场景中(一个相对常见的场景),似乎有可能具有一个集合类,该类将保留每个对象的“最后显示版本”以及更改队列。 ,并用于BeginInvoke运行一种方法,该方法将在UI线程中执行所有适当的更改[最多BeginInvoke在任何给定时间都将挂起。
2013年

1
甚至都不知道这存在!感谢您撰写本文!
凯利

15
一个小例子将使这个答案更加有用。我认为这可能是正确的解决方案,但我不知道如何实现。
RubberDuck

2
@Kohanz调用UI线程分派器有很多缺点。最大的问题是,直到UI线程实际处理分派之后,集合才会被更新,然后您将在UI线程上运行,这可能会导致响应性问题。另一方面,使用锁定方法,您可以立即更新集合,并且可以继续在后台线程上进行处理,而无需依赖UI线程执行任何操作。UI线程将根据需要在下一个渲染周期中赶上所做的更改。
Mike Marynowski

2
从此线程的答案中可以得到有关EnableCollectionSynchronization的更多见解:stackoverflow.com/a/16511740/2887274
Matthew S

22

使用.NET 4.0,您可以使用以下一线:

.Add

Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));

.Remove

Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));

11

后代的集合同步代码。这使用简单的锁定机制来启用集合同步。请注意,您必须在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);
        }
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.