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


68

在后台线程上更新业务对象的集合时,出现以下错误消息:

这种类型的CollectionView不支持从与Dispatcher线程不同的线程对其SourceCollection进行更改。

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


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

Answers:


64

以下是乔纳森发现的实现的改进。首先,它在与其关联的调度程序上运行每个事件处理程序,而不是假设它们都在同一(UI)调度程序上。其次,它使用BeginInvoke允许处理在我们等待调度程序可用之前继续进行。如果后台线程在每个线程之间进行大量更新和处理的情况下,这将使解决方案更快。也许更重要的是,它克服了等待调用时因阻塞而导致的问题(例如,将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,所以有可能在调用处理程序之前撤消通知的更改。这通常会导致“索引超出范围”。根据列表的新(更改)状态检查事件参数时引发的异常。为了避免这种情况,将所有延迟事件替换为Reset事件。在某些情况下,这可能导致过多的重画。


1
有点晚了,是一个老话题,但是这段代码为我省去了很多麻烦,谢谢!:)
KingTravisG 2013年

Caliburn在其BindableCollection <T>中也有一个非常好的实现。看看这里:caliburn.codeplex.com/SourceControl/changeset/view/...
Stephanvs

使用此版本时出现异常,但使用Jonathan提供的版本时却未出现异常。有谁知道为什么会这样?这是我的InnerException:引发此异常是因为名称为'OrdersGrid'的控件'System.Windows.Controls.DataGrid Items.Count:3'的生成器收到的CollectionChanged事件序列与Items的当前状态不一致采集。检测到以下差异:累计计数2与实际计数3不相同。
SoftwareFactor

@Nathan Phillips我知道这个线程要晚一年了,但是我正在使用MTObservableCollection实现,它运行得很好。但是,很少会间歇性地使该索引超出范围异常。您知道为什么会间歇性地发生这种情况吗?

这项工作很棒,为我省去了很多麻烦。使用了几个月,感觉就像在分享我的经验。我唯一有问题的一点是,调度程序几乎可以在他希望的任何时候运行,因此,如果我在偶尔为空或所有项目都不在集合内之后不久查询集合,就可以运行它。仍然很少发生。我确实需要100%无bug,所以我制作了一个用于检索集合的类,该类具有十分之一秒的线程睡眠,此后未发生错误。
法兰克2014年

87

采用:

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

简单,优雅,直截了当,喜欢它。。。谢谢。
PatriceCalvé2012年

8
Invoke在UI冻结中使用结果。使用BeginInvoke代替。
Xaqron

1
@MonsterMMORPG用.BeginInvoke代替.Invoke是一个很好的答案。
苋菜

17

Bea Stollnitz的这篇文章解释了错误消息以及其字样的原由。

编辑:从Bea的博客

不幸的是,此代码导致异常:“ NotSupportedException –这种类型的CollectionView不支持从与Dispatcher线程不同的线程对其SourceCollection进行更改。” 我了解到此错误消息使人们认为,如果他们使用的CollectionView不支持跨线程更改,那么他们必须找到能做到这一点的代码。嗯,此错误消息有点误导性:我们提供的现成的CollectionViews都不支持跨线程集合更改。而且,不幸的是,目前我们无法修复错误消息,我们非常受限制。


我更喜欢mark的实现,但是我不得不赞扬您找到最好的解释。
乔纳森·艾伦

7

找到一个。

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


3
请注意,这将导致每个集合更改的线程切换,并且所有更改都被序列化(这违背了使用后台线程的目的:-)。对于一些项目而言,这并不重要,但是如果您计划添加许多项目,则将大大降低性能。我通常将这些项目添加到后台线程中的另一个集合中,然后在计时器上将它们移动到gui集合中。
adrianm

1
我可以忍受这一点。我要避免的成本是首先获取项目,因为它将锁定UI。相比之下,将它们添加到集合中很便宜。
乔纳森·艾伦

@adrianm我对您的发言很感兴趣:在这种情况下,“串行化”是什么意思?您是否有“在计时器上移至gui集合”的示例?
杰拉德

对集合的所有更改都将导致dispatcher.Invoke,即在GUI线程上执行某些操作。这意味着两件事:1.工作者线程每次向集合添加内容时都必须停止并等待GUI线程。任务切换非常昂贵,并且会降低性能。2. GUI线程可能会阻塞导致GUI不响应的工作量。可以在此处stackoverflow.com/a/4530900/157224找到针对类似问题的基于计时器的解决方案。
adrianm 2014年


2

抱歉,无法添加评论,但所有这些都是错误的。

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

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

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

使用“重置”操作调用BeginInvoke时也存在问题。“重置”是处理程序应查看集合本身的唯一操作。如果您开始BeginInvoke一个“重置”,然后立即开始BeginInvoke几个“添加”动作,则处理程序将接受具有已更新集合的“重置”,而下一个“添加”将造成混乱。

这是我可行的实现。实际上,我正在考虑完全删除BeginInvoke:

快速执行和线程安全的可观察集合


2

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

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

这告诉WPF,可以从UI线程修改集合,因此它知道必须将任何UI更改整理回适当的线程。

如果您没有锁对象,则还会提供重载以提供同步回调。


1

如果您想定期更新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对象(而不是优先级)来实现构造函数!
Andry


0

它们都不是,只需使用Dispatcher.BeginInvoke


这违反了具有后台线程和独立数据层的目的。
乔纳森·艾伦

3
不,不是-所有工作都是获取数据/对其进行处理;您可以在后台线程中执行此操作,然后使用Dispatcher.BeginInvoke将其移动到集合中(希望花费很少的时间)。
安娜·贝茨

0

这是我在进行一些谷歌搜索和少量修改后制作的VB版本。为我工作。

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

  'from: http://stackoverflow.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

0

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

Dim obj As DispatcherObject = invocation.Target

通过

Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)
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.