是.NET ObservableCollection<> ToList()线程安全吗?如果没有,如何进行

时间:2017-03-17 17:00:18

标签: .net thread-safety observablecollection tolist

我有一个ObservableCollection日志,我已通过属性

绑定到我的GUI
public ObservableCollection<ILog> Logs {get; private set;}

需要在其他地方显示日志的子集,所以我有:

public ObservableCollection<ILog> LogsForDisplay
    {
        get
        {
            ObservableCollection<ILog> displayLogs = new ObservableCollection<ILog>();
            foreach (Log log in Logs.ToList()) // notice the ToList()
            {
                if (log.Date != DateTime.Now.Day)
                    continue;
                displayLogs.Add(log);
            }
            return displayLogs;

        }

在我添加“ToList()”之前,我偶尔会遇到“收集被修改;枚举操作可能无法执行”的例外情况。有意义的是,当我迭代它时,有人可以添加到Logs。我从Collection was modified; enumeration operation may not execute得到了“ToList”的想法,这似乎表明ToList是要走的路,并暗示它是线程安全的。但是ToList()线程安全吗?我假设在内部它必须使用列表并迭代它?如果有人同时添加该列表怎么办?仅仅因为我没有看到问题并不意味着没有问题。

我的问题。 ToList()线程是否安全?如果没有,保护日志的最佳模式是什么?如果ToList()是线程安全的,你有参考吗?

奖金问题。如果要求改变,我需要在GUI上显示的是LogsForDisplay和NOT Logs,我可以将Logs更改为其他可以解决问题的东西吗?如ImmutableList?然后我就不必调用ToList&lt;&gt;我认为这需要一些时间来制作副本。

如果我能澄清,请告诉我。 谢谢,

戴夫

6 个答案:

答案 0 :(得分:6)

ToList扩展方法的实现归结为通过Array.Copy方法将项​​目从一个数组复制到另一个数组,虽然隐藏Collection was modified错误并不是线程安全的,但是在Array.Copy calll期间更改基础项目时,可能会遇到奇怪的行为。

我建议使用CollectionView进行绑定,我在相似的情况下已经使用了很长时间并且到目前为止没有遇到任何问题。

// somewhere in .ctor or other init-code
var logsForDisplay = new CollectionView(this.Logs);
logsForDisplay.Predicate = log => ((Log)log).Date == DateTime.Now.Day;

public CollectionView LogsForDisplay { get { return this.logsForDisplay; } }

对于不同的用例,您可以使用另一个CollectionView,例如:

// somewhere in .ctor or other init-code
var yesterdaysLogs = new CollectionView(this.Logs);
yesterdaysLogs.Predicate = log => ((Log)log).Date == DateTime.Now.AddDays(-1).Day;

public CollectionView YesterdaysLogs{ get { return this.yesterdaysLogs; } }

答案 1 :(得分:1)

ToList扩展方法是&#34;线程安全&#34;当满足以下两个条件时:

  • Logs集合仅使用Add方法进行修改。也就是说,只能通过在集合的末尾添加项目。永远不会删除或插入项目。这可以保证迭代项目是安全的。
  • 满足以下两个条件之一:
    • 永远不会修改现有项目。
    • 可能会修改现有项目,但LogsForDisplay.get方法中不需要集合中所有项目的最新(一致)状态。

如果不满足这些条件,您必须使用ImmutableList作为ObservableCollection的基础集合或使用锁。

如果满足这两个条件,则您不必使用foreach并使用ToList<TSource>制作该集合的副本,您可以安全地使用for循环索引。

答案 2 :(得分:1)

正如alex.b所说,.ToList方法不是线程安全的,这个源代码链接证明了http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,d2ac2c19c9cf1d44

你有什么选择?嗯,你有很多:

  1. 如果你想坚持使用ObservableCollection并且100%安全,那么你必须用一个lock语句包装Logs.ToList()。当然,在这种情况下你需要用一个锁来包装任何修改<的进程strong>日志集合。
  2. 我注意到 LogsForDisplay 是一个可能绑定到WPF网格的只读(?)属性。如果您只想按需显示数据而不是每次Log集合更改时,您都可以轻松地将 Logs 的类型替换为线程安全集合,例如Immutable集合或来自的集合。像ConcurrentDictionary这样的System.Collections.Concurrent命名空间。因为您想要返回日志的子集,所以无法避免将项目复制到另一个列表中并在以后返回。即使在这种情况下,您仍然必须在调用.ToList()扩展名时使用Lock,但只能使用一次。

答案 3 :(得分:1)

ToList不是线程安全的,因为它是一个扩展方法,所以不太可能是线程安全的。这意味着它只能在某些扩展方法中提供线程安全性,这些扩展方法都会使用某些同步,但这不会防止对集合的直接线程不安全调用。请参阅实现here(已经引用)。

但你为什么要谈论线程安全呢? ObservableCollection本身不是线程安全的,所以它很奇怪你说一些并发操作可能是原始错误的来源。因此,如果您正确使用了Logs集合,则根本不必使用ToList。

答案 4 :(得分:1)

简单的解决方案是实现自己的线程安全的ObservableCollection, 一个简单的线程友好版本的可观察集合:

 public class NEWObservableCollection<T> : ObservableCollection<T>
    {
        public override event NotifyCollectionChangedEventHandler CollectionChanged;
        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            NotifyCollectionChangedEventHandler CollectionChanged = this.CollectionChanged;
            if (CollectionChanged != null)
                foreach (NotifyCollectionChangedEventHandler notifyCollectionChangedEventHandler in CollectionChanged.GetInvocationList())
                {
                    DispatcherObject dispatcherObject = notifyCollectionChangedEventHandler.Target as DispatcherObject;
                    if (dispatcherObject != null)
                    {
                        Dispatcher dispatcher = dispatcherObject.Dispatcher;
                        if (dispatcher != null && !dispatcher.CheckAccess())
                        {
                            dispatcher.BeginInvoke(
                                (Action)(() => notifyCollectionChangedEventHandler.Invoke(this,
                                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))),
                                DispatcherPriority.DataBind);
                            continue;
                        }
                    }
                    notifyCollectionChangedEventHandler.Invoke(this, e);
                }
        }
    }

答案 5 :(得分:1)

Enumerator.MoveNext循环中开始枚举集合后修改集合时,

forreach方法throws an exception。方法ToList将复制ObservableCollection的内部数组,因为源集合实现了ICollection<T>接口并且不会抛出此类异常,因为无法更改数组的大小。如果ObservableCollection被修改,同时您无法获得这些更改。但是你要返回的数据无论如何都是陈旧的。所以这是安全的方法,在你的情况下足够好。

链接,以便您可以检查并确保自己:Enumerable.csList.cs

@ alex.b提供的解决方案如果符合您的要求也会运作良好。

您不需要任何线程安全集合来执行此任务,因为它们只会增加额外的同步开销。