如何通过工作线程更新 ObservableCollection?
Posted
技术标签:
【中文标题】如何通过工作线程更新 ObservableCollection?【英文标题】:How do I update an ObservableCollection via a worker thread? 【发布时间】:2011-01-06 17:05:47 【问题描述】:我有一个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/… 【参考方案1】:.NET 4.5 的新选项
从 .NET 4.5 开始,有一个内置机制可以自动同步对集合的访问并将CollectionChanged
事件分派到 UI 线程。要启用此功能,您需要在您的 UI 线程中调用 BindingOperations.EnableCollectionSynchronization
。
EnableCollectionSynchronization
做了两件事:
-
记住调用它的线程并导致数据绑定管道在该线程上编组
CollectionChanged
事件。
在处理完编组事件之前获取对集合的锁定,以便运行 UI 线程的事件处理程序在从后台线程修改集合时不会尝试读取集合。
非常重要的是,这并不能解决所有问题:为了确保线程安全地访问本质上不是线程安全的集合您必须通过以下方式与框架合作当集合即将被修改时,从后台线程获取相同的锁。
因此正确操作所需的步骤是:
1。确定您将使用哪种锁定方式
这将确定必须使用EnableCollectionSynchronization
的哪个重载。大多数情况下,一个简单的lock
语句就足够了,所以this overload 是标准选择,但如果您使用一些花哨的同步机制,还有support for custom locks。
2。创建集合并启用同步
根据选择的锁定机制,在 UI 线程上调用适当的重载。如果使用标准的lock
语句,您需要提供锁定对象作为参数。如果使用自定义同步,您需要提供CollectionSynchronizationCallback
委托和上下文对象(可以是null
)。调用时,此委托必须获取您的自定义锁,调用传递给它的 Action
并在返回之前释放锁。
3。通过在修改之前锁定集合进行合作
当您要自己修改集合时,您还必须使用相同的机制锁定集合;在简单场景中使用lock()
在传递给EnableCollectionSynchronization
的同一个锁对象上执行此操作,或者在自定义场景中使用相同的自定义同步机制。
【讨论】:
这是否会导致集合更新阻塞,直到 UI 线程开始处理它们?在涉及不可变对象的单向数据绑定集合的场景中(一种相对常见的场景),似乎可以有一个集合类来保留每个对象的“最后显示的版本”以及一个更改队列, 并使用BeginInvoke
运行一个方法,该方法将在 UI 线程中执行所有适当的更改 [在任何给定时间最多有一个 BeginInvoke
处于挂起状态。
我不清楚这对必要时调用 Dispatcher 有何改进。它们显然是不同的方法,似乎都需要大约相同数量的额外代码 - 因此解释为什么一个比另一个更受欢迎会很有帮助。在我的脑海中,似乎调用调度程序失败会导致更可预测的失败(可能会被捕获和修复),而忘记同步对集合更改的访问可能会丢失,直到具有不同的环境遇到计时行为。
@Kohanz 调用 UI 线程调度程序有许多缺点。最大的问题是,在 UI 线程实际处理调度之前,您的集合不会更新,然后您将在 UI 线程上运行,这可能会导致响应性问题。另一方面,使用锁定方法,您可以立即更新集合,并且可以继续在后台线程上进行处理,而无需依赖 UI 线程做任何事情。 UI 线程将根据需要在下一个渲染周期赶上变化。
我已经研究 4.5 中的集合同步大约一个月了,我认为这个答案有些不正确。答案指出启用调用必须发生在 UI 线程上,并且回调发生在 UI 线程上。这两种情况似乎都不是。我能够在后台线程上启用集合同步并且仍然使用这种机制。此外,框架中的深度调用不进行任何编组(参见 ViewManager.AccessCollection.referencesource.microsoft.com/#PresentationFramework/src/…)
关于EnableCollectionSynchronization的这个帖子的答案有更多见解:***.com/a/16511740/2887274【参考方案2】:
从技术上讲,问题不在于您正在从后台线程更新 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 线程? 是的,BackgroundWorker 的设计方式是使用 SynchronizationContext.Current 来引发其完成和进度事件。 DoWork 事件将在后台线程上运行。这是一篇关于 WPF 中线程的好文章,它也讨论了 BackgroundWorker msdn.microsoft.com/en-us/magazine/cc163328.aspx#S4 这个答案很简单。感谢分享! @Michael 在大多数情况下,后台线程不应阻塞并等待 UI 更新。如果两个线程最终相互等待,使用 Dispatcher.Invoke 会冒死锁的风险,并且充其量只会显着阻碍代码的性能。在您的特定情况下,您可能需要这样做,但对于绝大多数情况,您的最后一句话根本不正确。 这种方式最终会导致难以调试的并发问题和应用程序崩溃。【参考方案3】:在 .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)));
【讨论】:
这假定 Application.Current 不为空。【参考方案4】:为后代收集同步代码。这使用简单的锁定机制来启用集合同步。请注意,您必须在 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);
【讨论】:
【参考方案5】:我使用了 SynchronizationContext:
SynchronizationContext SyncContext get; set;
//在构造函数中:
SyncContext = SynchronizationContext.Current;
//在Background Worker或Event Handler中:
SyncContext.Post(o =>
ObservableCollection.AddRange(myData);
, null);
【讨论】:
【参考方案6】:@Jon 回答不错,但缺少代码示例:
// UI thread
var myCollection = new ObservableCollection<string>();
var lockObject = new object();
BindingOperations.EnableCollectionSynchronization(myCollection, lockObject );
[..]
// Non UI thread
lock (lockObject)
myCollection.Add("Foo")
另请注意,CollectionChanged
事件处理程序仍会从非 UI 线程调用。
【讨论】:
@LadderLogic 的回答已经在 3 年前提供了一个例子。以上是关于如何通过工作线程更新 ObservableCollection?的主要内容,如果未能解决你的问题,请参考以下文章