我应该从哪里获取一个线程安全的CollectionView?

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

此类型的CollectionView不支持从与Dispatcher线程不同的线程更改其SourceCollection。

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

1
请尝试以下链接,该链接提供了一个线程安全的解决方案,可以从任何线程工作,并且可以通过多个UI线程进行绑定:http://www.codeproject.com/Articles/64936/Multithreaded-ObservableImmutableCollection - Anthony
12个回答

89

使用:

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

简单、优雅、直接,喜欢它……学会了就不会忘。谢谢。 - Patrice Calvé
8
使用 Invoke 会导致界面冻结,请改用 BeginInvoke - Xaqron
1
@MonsterMMORPG 使用 .BeginInvoke 而不是 .Invoke 的解决方案是一个好答案。 - amaranth

64
下面是对Jonathan实现的改进。首先,它在与事件处理程序关联的调度程序上运行每个事件处理程序,而不是假设它们都在同一个(UI)调度程序上。其次,它使用BeginInvoke允许在等待调度程序可用时继续处理。这使得解决方案在后台线程在每个更新之间进行处理的情况下更快。可能更重要的是,它克服了在等待Invoke时阻塞引起的问题(例如,在使用ConcurrencyMode.Single的WCF时可能会发生死锁)。
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,所以在处理程序调用之前被通知的更改可能会被撤消。当事件参数与列表的新(更改后)状态进行检查时,这通常会导致“索引超出范围”异常的抛出。为了避免这种情况,所有延迟事件都将被替换为重置事件。这可能会在某些情况下导致过多的重绘。


1
有点晚了,而且是一个老话题,但这段代码为我节省了很多麻烦,谢谢! :) - KingTravisG
我使用这个版本时出现异常,但使用Jonathan提供的版本则没有。有人有想法吗?以下是我的InnerException:这个异常是由于名为“OrdersGrid”的控件'System.Windows.Controls.DataGrid Items.Count:3'的生成器接收到与Items集合的当前状态不一致的CollectionChanged事件序列而引发的。 检测到以下差异: 累计计数2与实际计数3不同。 [累积计数是(上次重置时的计数+#Adds-#Removes)。] - SoftwareFactor
@Nathan Phillips,我知道我来晚了一年,但我正在使用你的MTObservableCollection实现,它运行得非常好。然而,偶尔会出现索引超出范围异常。你有任何想法为什么会间歇性地发生吗? - user1795804
这个工具非常好用,帮我省了很多麻烦。我已经使用了几个月,感觉应该分享一下我的使用经验。唯一的小问题是调度程序几乎随时都在运行,所以如果我查询集合后不久,有时会出现空集合或所有项尚未在集合中。但这种情况还是比较少见的。我需要一个100%无bug的解决方案,所以我编写了一个类来检索集合,并且该类有一个0.1秒的线程休眠时间,自从那时起就没有出现错误了。 - Franck
这是因为这个解决方案不足以保证线程安全。请尝试以下链接,该链接提供了一个线程安全的解决方案,可以从任何线程工作,并且可以通过多个UI线程进行绑定:http://www.codeproject.com/Articles/64936/Multithreaded-ObservableImmutableCollection - Anthony
显示剩余10条评论

17

这篇文章由Bea Stollnitz撰写,解释了“NotSupportedException – This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.” 这个错误信息的含义以及为什么会出现这种措辞。

编辑:来自Bea的博客

很遗憾,这段代码会导致异常:“NotSupportedException - 这种类型的CollectionView不支持在与Dispatch线程不同的线程中对其SourceCollection进行更改。” 我理解这个错误消息会让人们认为,如果他们正在使用的CollectionView不支持跨线程更改,那么他们就必须找到支持跨线程更改的CollectionView。好吧,这个错误消息有点误导人:我们提供的所有开箱即用的CollectionView都不支持跨线程集合更改。而且很不幸,在这一点上我们无法修复错误消息,我们被完全限制了。


我更喜欢马克的实现方式,但是我必须给你找到最好的解释以示赞赏。 - Jonathan Allen

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
我可以接受这个。我试图避免的成本是首先获取项目,因为它会锁定用户界面。相比之下,将它们添加到集合中则更加便宜。 - Jonathan Allen
@adrianm 我对你的评论很感兴趣:在这种情况下,你所说的“序列化”是什么意思?你有“定时器移动到GUI集合”的例子吗? - Gerard
所有对集合的更改都会导致dispatcher.Invoke,即在GUI线程上执行某些操作。这意味着两件事:1.每次向集合添加内容时,工作线程必须停止并等待GUI线程。任务切换是昂贵的,会降低性能。2.GUI线程可能会因为工作量过大而无响应。类似问题的基于计时器的解决方案可以在此处找到https://dev59.com/lW855IYBdhLWcg3wIAga#4530900。 - adrianm

3

2
抱歉,无法添加评论,但所有这些都是错误的。
ObservableCollection 不是线程安全的。不仅因为调度程序问题,而且它根本就不是线程安全的(来自 MSDN):
任何此类型的公共静态(在 Visual Basic 中为 Shared)成员都是线程安全的。任何实例成员不能保证是线程安全的。
请看这里:http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx 当使用“Reset”操作调用 BeginInvoke 时,也存在问题。 “Reset” 是唯一一个处理程序应查看集合本身的操作。如果您使用“Reset”并立即使用几个“Add”操作调用 BeginInvoke,则处理程序将接受已更新集合的“Reset”,下一个“Add”将创建混乱。
这是我的实现,可以正常工作。实际上,我正在考虑完全删除 BeginInvoke: Fast performing and thread safe observable collection

2

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

Original Answer翻译成"最初的回答"

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

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

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


注:Original Answer翻译成“最初的回答”为错误翻译。

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,在创建计时器实例时,为了使其工作,你需要将应用程序调度器传递给它!你可以在构造函数中通过传递除了优先级之外的System.Windows.Application.Current.Dispatcher对象来实现! - Andry

1

Try This:

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

 //Code

}));

0

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

Dim obj As DispatcherObject = invocation.Target

通过

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接