【问题标题】:Is .NET ObservableCollection<> ToList() thread safe? If not, how to proceed.NET ObservableCollection<> ToList() 线程安全吗?如果没有,如何进行
【发布时间】:2017-03-17 17:00:18
【问题描述】:

我有一个已通过属性绑定到我的 GUI 的 ObservableCollection 日志

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()”之前,我偶尔会遇到关于“集合已修改;枚举操作可能无法执行”的异常情况,这是有道理的——有人可以在我对其进行迭代时添加到日志中。我从Collection was modified; enumeration operation may not execute 得到了“ToList”的想法,这似乎表明 ToList 是要走的路,并暗示它是线程安全的。但是 ToList() 线程安全吗?我假设在内部它必须使用列表并对其进行迭代?如果有人同时添加到该列表中怎么办?仅仅因为我没有看到问题并不意味着没有问题。

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

奖金问题。如果要求发生变化并且我需要在 GUI 上显示的只是 LogsForDisplay 而不是 Logs,我可以将 Logs 更改为可以解决问题的其他内容吗?比如 ImmutableList ?然后我就不必调用 ToList 我认为这需要一些时间来制作副本。

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

戴夫

【问题讨论】:

  • documentation 不保证任何有用的方法或属性都是线程安全的,因此您必须假设它们都不是。您可以在您的日志记录方法中使用lock(您将在其中添加到Logs 集合)以及有问题的ToList。这似乎是唯一确定的解决方法。可能覆盖您的类中从ObservableCollection 派生的所有插入等方法,以使用SyncRoot 属性进行锁定。
  • 所有好的答案,我感谢大家!可惜只能选1个。

标签: .net thread-safety observablecollection


【解决方案1】:

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; } }

【讨论】:

    【解决方案2】:

    简单的解决方案是实现自己的线程安全 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);
                    }
            }
        }
    

    【讨论】:

      【解决方案3】:

      当满足以下两个条件时,ToList 扩展方法是“线程安全的”:

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

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

      如果满足这两个条件,您不必使用foreach 并使用ToList&lt;TSource&gt; 制作集合的副本,您可以安全地使用带有索引的for 循环。

      【讨论】:

        【解决方案4】:

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

        你有什么选择?好吧,你有很多:

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

        【讨论】:

          【解决方案5】:

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

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

          【讨论】:

            【解决方案6】:

            Enumerator.MoveNext 方法 throws an exception 在您开始在 forreach 循环中枚举它后修改集合时。方法ToList 将复制ObservableCollection 的内部数组,因为源集合实现了ICollection&lt;T&gt; 接口并且不会抛出此类异常,因为数组的大小无法更改。如果同时修改了ObservableCollection,您将不会获得这些更改。但是无论如何,您要返回的数据都是陈旧的。因此,在您的情况下,这是安全的方法并且足够好。

            链接,以便您检查和确保自己:Enumerable.cs 和 List.cs

            @alex.b 提供的解决方案也可以很好地满足您的要求。

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2014-05-31
              • 1970-01-01
              • 1970-01-01
              • 2013-06-10
              • 2010-09-28
              • 1970-01-01
              • 2021-12-27
              • 2011-08-02
              相关资源
              最近更新 更多