我在哪里可以获得线程安全的 CollectionView?

Posted

技术标签:

【中文标题】我在哪里可以获得线程安全的 CollectionView?【英文标题】:Where do I get a thread-safe CollectionView? 【发布时间】:2011-01-09 09:57:10 【问题描述】:

在后台线程上更新业务对象集合时,我收到以下错误消息:

这种类型的 CollectionView 不支持从不同于 Dispatcher 线程的线程更改其 SourceCollection。

好的,这是有道理的。但它也引出了一个问题,CollectionView 的哪个版本支持多线程以及如何让我的对象使用它?

【问题讨论】:

试试下面的链接,它提供了一个线程安全的解决方案,可以在任何线程上工作,并且可以通过多个 UI 线程绑定:codeproject.com/Articles/64936/… 【参考方案1】:

没有,只使用 Dispatcher.BeginInvoke

【讨论】:

这违背了拥有后台线程和独立数据层的目的。 不,它没有——所有的工作都是获取数据/处理它;您在后台线程中执行此操作,然后使用 Dispatcher.BeginInvoke 将其移动到集合中(希望这需要很少的时间)。【参考方案2】:

找到一个。

public class MTObservableCollection<T> : ObservableCollection<T>

   public override event NotifyCollectionChangedEventHandler CollectionChanged;
   protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
   
      var eh = CollectionChanged;
      if (eh != null)
      
         Dispatcher dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
                 let dpo = nh.Target as DispatcherObject
                 where dpo != null
                 select dpo.Dispatcher).FirstOrDefault();

        if (dispatcher != null && dispatcher.CheckAccess() == false)
        
           dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => OnCollectionChanged(e)));
        
        else
        
           foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList())
              nh.Invoke(this, e);
        
     
  

http://www.julmar.com/blog/mark/2009/04/01/AddingToAnObservableCollectionFromABackgroundThread.aspx

【讨论】:

请注意,这将导致每个集合更改的线程切换,并且所有更改都被序列化(这违背了拥有后台线程的目的:-))。对于一些项目,这无关紧要,但如果您计划添加许多项目,则会大大损害性能。我通常在后台线程中将项目添加到另一个集合中,然后在计时器上将它们移动到 gui 集合中。 我可以忍受。我试图避免的成本是首先获取项目,因为它会锁定 UI。相比之下,将它们添加到集合中很便宜。 @adrianm 我对您的评论很感兴趣:在这种情况下,您所说的“序列化”是什么意思?你有“在计时器上移动到 gui 集合”的例子吗? 对集合的所有更改都将导致dispatcher.Invoke,即在 GUI 线程上执行某些操作。这意味着两件事: 1. 工作线程每次向集合中添加内容时都必须停止并等待 GUI 线程。任务切换很昂贵,并且会降低性能。 2. GUI 线程可能会阻塞导致 GUI 无响应的工作量。类似问题的基于计时器的解决方案可以在这里找到***.com/a/4530900/157224。【参考方案3】:

This Bea Stollnitz 的帖子解释了那个错误消息以及为什么它的措辞如此。

编辑:来自 Bea 的博客

不幸的是,此代码会导致异常:“NotSupportedException - 这种类型的 CollectionView 不支持从不同于 Dispatcher 线程的线程更改其 SourceCollection。”我理解这个错误信息让人们认为,如果他们使用的 CollectionView 不支持跨线程更改,那么他们必须找到支持的那个。好吧,这个错误消息有点误导:我们提供的开箱即用的 CollectionView 都不支持跨线程集合更改。不,很遗憾,我们目前无法修复错误消息,我们已被锁定。

【讨论】:

我更喜欢马克的实现,但我必须感谢你找到了最好的解释。【参考方案4】:

用途:

System.Windows.Application.Current.Dispatcher.Invoke(
    System.Windows.Threading.DispatcherPriority.Normal,
    (Action)delegate() 
    
         // Your Action Code
    );

【讨论】:

简单,优雅,直截了当,喜欢它.... 学到了它。谢谢。 使用 Invoke 会导致 UI 冻结。请改用BeginInvoke @MonsterMMORPG 这个解决方案用 .BeginInvoke 代替 .Invoke 是很好的答案。【参考方案5】:

如果您想定期更新 WPF UI 控件并同时使用 UI,您可以使用 DispatcherTimer

XAML

<Grid>
        <DataGrid AutoGenerateColumns="True" Height="200" HorizontalAlignment="Left" Name="dgDownloads" VerticalAlignment="Top" Width="548" />
        <Label Content="" Height="28" HorizontalAlignment="Left" Margin="0,221,0,0" Name="lblFileCouner" VerticalAlignment="Top" Width="173" />
</Grid>

C#

 public partial class DownloadStats : Window
    
        private MainWindow _parent;

        DispatcherTimer timer = new DispatcherTimer();

        ObservableCollection<FileView> fileViewList = new ObservableCollection<FileView>();

        public DownloadStats(MainWindow parent)
        
            InitializeComponent();

            _parent = parent;
            Owner = parent;

            timer.Interval = new TimeSpan(0, 0, 1);
            timer.Tick += new EventHandler(timer_Tick);
            timer.Start();
        

        void timer_Tick(object sender, EventArgs e)
        
            dgDownloads.ItemsSource = null;
            fileViewList.Clear();

            if (_parent.contentManagerWorkArea.Count > 0)
            
                foreach (var item in _parent.contentManagerWorkArea)
                
                    FileView nf = item.Value.FileView;

                    fileViewList.Add(nf);
                
            

            if (fileViewList.Count > 0)
            
                lblFileCouner.Content = fileViewList.Count;
                dgDownloads.ItemsSource = fileViewList;
            
           

    

【讨论】:

这是一个很好的解决方案但是有一个错误 Clark,当你创建计时器的实例时,为了让它工作,你需要将 Application Dispatcher 传递给它!除了优先级之外,您可以在构造函数中传递 System.Windows.Application.Current.Dispatcher 对象!【参考方案6】:

以下是对 Jonathan 发现的实现的改进。首先,它在与其关联的调度程序上运行每个事件处理程序,而不是假设它们都在同一个(UI)调度程序上。其次,它使用 BeginInvoke 允许在我们等待调度程序可用时继续处理。这使得解决方案在后台线程进行大量更新并在每个更新之间进行处理的情况下更快。也许更重要的是,它克服了等待 Invoke 时因阻塞而导致的问题(例如,在将 WCF 与 ConcurrencyMode.Single 一起使用时可能会发生死锁)。

public class MTObservableCollection<T> : ObservableCollection<T>

    public override event NotifyCollectionChangedEventHandler CollectionChanged;
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    
        NotifyCollectionChangedEventHandler CollectionChanged = this.CollectionChanged;
        if (CollectionChanged != null)
            foreach (NotifyCollectionChangedEventHandler nh in CollectionChanged.GetInvocationList())
            
                DispatcherObject dispObj = nh.Target as DispatcherObject;
                if (dispObj != null)
                
                    Dispatcher dispatcher = dispObj.Dispatcher;
                    if (dispatcher != null && !dispatcher.CheckAccess())
                    
                        dispatcher.BeginInvoke(
                            (Action)(() => nh.Invoke(this,
                                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))),
                            DispatcherPriority.DataBind);
                        continue;
                    
                
                nh.Invoke(this, e);
            
    

因为我们使用的是 BeginInvoke,所以通知的更改可能在调用处理程序之前被撤消。这通常会导致“索引超出范围”。根据列表的新(更改)状态检查事件参数时引发异常。为了避免这种情况,所有延迟事件都替换为重置事件。在某些情况下,这可能会导致过度重绘。

【讨论】:

有点晚了,老话题了,但是这段代码让我省了很多麻烦,谢谢! :) Caliburn 在他们的 BindableCollection 中也有一个非常好的实现。看这里:caliburn.codeplex.com/SourceControl/changeset/view/… 我在使用这个版本时遇到了异常,但在使用 Jonathan 提供的版本时却没有。有谁知道为什么会这样?这是我的 InnerException:引发此异常是因为名称为“OrdersGrid”的控件“System.Windows.Controls.DataGrid Items.Count:3”的生成器已收到与 Items 的当前状态不一致的 CollectionChanged 事件序列收藏。检测到以下差异:累积计数 2 与实际计数 3 不同。[累积计数为 (Count at last Reset + #Adds - #Removes since last Reset)。 @Nathan Phillips 我知道我在这个线程上迟到了一年,但我正在使用你的 MTObservableCollection 实现,它工作得很好。但是,我很少会间歇性地使该索引超出范围异常。您知道为什么会间歇性发生这种情况吗? 这项工作很棒,为我省去了很多麻烦。已经使用了几个月,并想分享我的经验。我唯一遇到的问题是调度员几乎可以随时运行,所以如果我在集合偶尔为空或所有项目都不在集合中之后不久查询集合。还是比较少见的。我确实需要 100% 没有错误,所以我创建了一个类来检索集合,并且该类的线程睡眠时间为十分之一秒,并且此后没有发生错误。【参考方案7】:

试试这个:

this.Dispatcher.Invoke(DispatcherPriority.Background, new Action(
() =>


 //Code

));

【讨论】:

【参考方案8】:

你也可以看看:BindingOperations.EnableCollectionSynchronization

见Upgrading to .NET 4.5: An ItemsControl is inconsistent with its items source

【讨论】:

【参考方案9】:

这是我经过一些谷歌搜索和轻微修改后制作的 VB 版本。为我工作。

  Imports System.Collections.ObjectModel
  Imports System.Collections.Specialized
  Imports System.ComponentModel
  Imports System.Reflection
  Imports System.Windows.Threading

  'from: http://***.com/questions/2137769/where-do-i-get-a-thread-safe-collectionview
  Public Class ThreadSafeObservableCollection(Of T)
    Inherits ObservableCollection(Of T)

    'from: http://geekswithblogs.net/NewThingsILearned/archive/2008/01/16/listcollectionviewcollectionview-doesnt-support-notifycollectionchanged-with-multiple-items.aspx
    Protected Overrides Sub OnCollectionChanged(ByVal e As System.Collections.Specialized.NotifyCollectionChangedEventArgs)
      Dim doit As Boolean = False

      doit = (e.NewItems IsNot Nothing) AndAlso (e.NewItems.Count > 0)
      doit = doit OrElse ((e.OldItems IsNot Nothing) AndAlso (e.OldItems.Count > 0))

      If (doit) Then
        Dim handler As NotifyCollectionChangedEventHandler = GetType(ObservableCollection(Of T)).GetField("CollectionChanged", BindingFlags.Instance Or BindingFlags.NonPublic).GetValue(Me)
        If (handler Is Nothing) Then
          Return
        End If

        For Each invocation As NotifyCollectionChangedEventHandler In handler.GetInvocationList
          Dim obj As DispatcherObject = invocation.Target

          If (obj IsNot Nothing) Then
            Dim disp As Dispatcher = obj.Dispatcher
            If (disp IsNot Nothing AndAlso Not (disp.CheckAccess())) Then
              disp.BeginInvoke(
                Sub()
                  invocation.Invoke(Me, New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
                End Sub, DispatcherPriority.DataBind)
              Continue For
            End If
          End If

          invocation.Invoke(Me, e)
        Next
      End If
    End Sub
  End Class

【讨论】:

【参考方案10】:

抱歉,无法添加评论,但这一切都是错误的。

ObservableCollection 不是线程安全的。不仅因为这个调度程序问题,而且它根本不是线程安全的(来自 msdn):

此类型的任何公共静态(在 Visual Basic 中为共享)成员都是线程安全的。不保证任何实例成员都是线程安全的。

看这里http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx

使用“重置”操作调用 BeginInvoke 时也会出现问题。 “重置”是处理程序应该查看集合本身的唯一操作。如果您 BeginInvoke 一个“Reset”,然后立即 BeginInvoke 几个“Add”操作,那么处理程序将接受一个具有已更新集合的“Reset”,并且下一个“Add”将造成混乱。

这是我的有效实现。实际上我正在考虑完全删除 BeginInvoke:

Fast performing and thread safe observable collection

【讨论】:

【参考方案11】:

VB版的小错误。只需替换:

Dim obj As DispatcherObject = invocation.Target

Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)

【讨论】:

【参考方案12】:

您可以通过启用集合同步来让 wpf 管理对集合的跨线程更改,如下所示:

BindingOperations.EnableCollectionSynchronization(collection, syncLock);
listBox.ItemsSource = collection;

这告诉 WPF 集合可能会在 UI 线程之外进行修改,因此它知道必须将任何 UI 更改编组回适当的线程。

如果你没有锁对象,还有一个重载来提供同步回调。

【讨论】:

以上是关于我在哪里可以获得线程安全的 CollectionView?的主要内容,如果未能解决你的问题,请参考以下文章

我在哪里可以在 kinect v2 中获得 BoneOrientation.StartJoint

iOS多线程到底不安全在哪里?

我在哪里可以获得用于 NHibernate 的 Linq?

我在哪里可以获得 comdef.h?

我在哪里可以获得 libpq 源?

亚马逊支付集成:我在哪里可以获得 preSharedEncodedKey?