我有一个可观察的日志集合,我已通过属性绑定到我的 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
你有什么选择?好吧,你有很多:
- 如果你想坚持使用ObservableCollection并保持100%的安全,那么你必须用一个锁定语句包装Logs.ToList()。当然,在这种情况下,您需要用锁包装任何修改日志收集的进程。
- 我注意到LogsForDisplay是一个只读 (?) 属性,可能绑定到 WPF 网格。如果只想按需显示数据,而不是每次日志集合更改时都显示数据,则可以轻松地将日志的类型替换为线程安全集合(如不可变集合)或 System.Collections.Concurrent 命名空间中的集合(如 ConcurrentDictionary)。由于您要返回日志的子集,因此无法避免将项目复制到另一个列表中并在以后返回。即使在这种情况下,在调用 .ToList() 扩展名,但只有一次。
ToList 不是线程安全的,并且不太可能是线程安全的,因为它是一个扩展方法。这意味着它只能在一组扩展方法中提供线程安全,这些方法都将使用一些同步,但这不会防止对集合进行直接线程不安全调用。请参阅此处的实现(如已引用)。
但是你为什么要谈论线程安全呢?ObservableCollection 本身不是线程安全的,所以奇怪的是你说一些并发操作可能是原始错误的根源。因此,如果正确使用了日志集合,则根本不需要使用 ToList。
Enumerator.MoveNext
方法在开始在循环中枚举集合后修改集合时引发异常forreach
。方法ToList
将复制ObservableCollection
的内部数组,因为源集合实现了ICollection<T>
接口,并且不会引发此类异常,因为数组的大小无法更改。如果同时修改了ObservableCollection
,您将不会获得这些更改。但是无论如何,您要返回的数据都是过时的。因此,这是安全的方法,在您的情况下已经足够好了。
链接,以便您可以检查并确保自己:可枚举.cs和列表.cs
如果 @alex.b 提供的解决方案满足您的要求,它也可以很好地工作。
此任务不需要任何线程安全集合,因为它们只会增加额外的同步开销。