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



我有一个可观察的日志集合,我已通过属性绑定到我的 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()"之前,我偶尔会收到有关"集合已修改;枚举操作可能无法执行" 有道理 - 有人可以在我迭代它时添加到日志中。 我从收藏中得到了"ToList"的想法被修改了;枚举操作可能无法执行,这似乎表明 ToList 是要走的路,并暗示它是线程安全的。 但是 ToList() 线程安全吗? 我假设在内部它必须使用该列表并迭代它? 如果有人同时添加到该列表中怎么办? 仅仅因为我没有看到问题并不意味着没有问题。

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

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

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

戴夫

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

我建议它使用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; } }

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

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

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

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

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

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

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

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

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

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

Enumerator.MoveNext

方法在开始在循环中枚举集合后修改集合时引发异常forreach。方法ToList将复制ObservableCollection的内部数组,因为源集合实现了ICollection<T>接口,并且不会引发此类异常,因为数组的大小无法更改。如果同时修改了ObservableCollection,您将不会获得这些更改。但是无论如何,您要返回的数据都是过时的。因此,这是安全的方法,在您的情况下已经足够好了。

链接,以便您可以检查并确保自己:可枚举.cs和列表.cs

如果 @alex.b 提供的解决方案满足您的要求,它也可以很好地工作。

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

相关内容

  • 没有找到相关文章

最新更新