我无法找到这个错误的根源,因为当附加调试器时,它似乎没有发生。
修改集合;枚举操作可能无法执行
下面是代码。
这是Windows服务中的WCF服务器。只要有数据事件,服务就会调用NotifySubscribers()方法(随机间隔,但不经常——大约每天800次)。
When a Windows Forms client subscribes, the subscriber ID is added to the subscribers dictionary, and when the client unsubscribes, it is deleted from the dictionary. The error happens when (or after) a client unsubscribes. It appears that the next time the NotifySubscribers() method is called, the foreach() loop fails with the error in the subject line. The method writes the error into the application log as shown in the code below. When a debugger is attached and a client unsubscribes, the code executes fine.
您认为这段代码有问题吗?我需要使字典线程安全吗?
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class SubscriptionServer : ISubscriptionServer
{
private static IDictionary<Guid, Subscriber> subscribers;
public SubscriptionServer()
{
subscribers = new Dictionary<Guid, Subscriber>();
}
public void NotifySubscribers(DataRecord sr)
{
foreach(Subscriber s in subscribers.Values)
{
try
{
s.Callback.SignalData(sr);
}
catch (Exception e)
{
DCS.WriteToApplicationLog(e.Message,
System.Diagnostics.EventLogEntryType.Error);
UnsubscribeEvent(s.ClientId);
}
}
}
public Guid SubscribeEvent(string clientDescription)
{
Subscriber subscriber = new Subscriber();
subscriber.Callback = OperationContext.Current.
GetCallbackChannel<IDCSCallback>();
subscribers.Add(subscriber.ClientId, subscriber);
return subscriber.ClientId;
}
public void UnsubscribeEvent(Guid clientId)
{
try
{
subscribers.Remove(clientId);
}
catch(Exception e)
{
System.Diagnostics.Debug.WriteLine("Unsubscribe Error " +
e.Message);
}
}
}
我想指出另一个没有在答案中反映出来的例子。我有一个字典<Tkey,TValue>共享在一个多线程应用程序,它使用ReaderWriterLockSlim来保护读写操作。这是一个引发异常的读取方法:
public IEnumerable<Data> GetInfo()
{
List<Data> info = null;
_cacheLock.EnterReadLock();
try
{
info = _cache.Values.SelectMany(ce => ce.Data); // Ad .Tolist() to avoid exc.
}
finally
{
_cacheLock.ExitReadLock();
}
return info;
}
一般来说,它工作得很好,但有时会出现异常。这个问题是LINQ的一个微妙之处:这段代码返回一个IEnumerable<Info>,它在离开锁保护的部分后仍然没有被枚举。因此,它可以在被枚举之前由其他线程更改,从而导致异常。解决方案是强制枚举,例如注释中显示的. tolist()。这样,可枚举对象在离开受保护的部分之前就已经被枚举了。
因此,如果在多线程应用程序中使用LINQ,请注意始终在离开受保护区域之前实现查询。
我想指出另一个没有在答案中反映出来的例子。我有一个字典<Tkey,TValue>共享在一个多线程应用程序,它使用ReaderWriterLockSlim来保护读写操作。这是一个引发异常的读取方法:
public IEnumerable<Data> GetInfo()
{
List<Data> info = null;
_cacheLock.EnterReadLock();
try
{
info = _cache.Values.SelectMany(ce => ce.Data); // Ad .Tolist() to avoid exc.
}
finally
{
_cacheLock.ExitReadLock();
}
return info;
}
一般来说,它工作得很好,但有时会出现异常。这个问题是LINQ的一个微妙之处:这段代码返回一个IEnumerable<Info>,它在离开锁保护的部分后仍然没有被枚举。因此,它可以在被枚举之前由其他线程更改,从而导致异常。解决方案是强制枚举,例如注释中显示的. tolist()。这样,可枚举对象在离开受保护的部分之前就已经被枚举了。
因此,如果在多线程应用程序中使用LINQ,请注意始终在离开受保护区域之前实现查询。
下面是一个特定的场景,需要使用专门的方法:
字典经常被列举出来。
字典很少被修改。
In this scenario creating a copy of the Dictionary (or the Dictionary.Values) before every enumeration can be quite costly. My idea about solving this problem is to reuse the same cached copy in multiple enumerations, and watch an IEnumerator of the original Dictionary for exceptions. The enumerator will be cached along with the copied data, and interrogated before starting a new enumeration. In case of an exception the cached copy will be discarded, and a new one will be created. Here is my implementation of this idea:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
public class EnumerableSnapshot<T> : IEnumerable<T>, IDisposable
{
private IEnumerable<T> _source;
private IEnumerator<T> _enumerator;
private ReadOnlyCollection<T> _cached;
public EnumerableSnapshot(IEnumerable<T> source)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
}
public IEnumerator<T> GetEnumerator()
{
if (_source == null) throw new ObjectDisposedException(this.GetType().Name);
if (_enumerator == null)
{
_enumerator = _source.GetEnumerator();
_cached = new ReadOnlyCollection<T>(_source.ToArray());
}
else
{
var modified = false;
if (_source is ICollection collection) // C# 7 syntax
{
modified = _cached.Count != collection.Count;
}
if (!modified)
{
try
{
_enumerator.MoveNext();
}
catch (InvalidOperationException)
{
modified = true;
}
}
if (modified)
{
_enumerator.Dispose();
_enumerator = _source.GetEnumerator();
_cached = new ReadOnlyCollection<T>(_source.ToArray());
}
}
return _cached.GetEnumerator();
}
public void Dispose()
{
_enumerator?.Dispose();
_enumerator = null;
_cached = null;
_source = null;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public static class EnumerableSnapshotExtensions
{
public static EnumerableSnapshot<T> ToEnumerableSnapshot<T>(
this IEnumerable<T> source) => new EnumerableSnapshot<T>(source);
}
使用的例子:
private static IDictionary<Guid, Subscriber> _subscribers;
private static EnumerableSnapshot<Subscriber> _subscribersSnapshot;
//...(in the constructor)
_subscribers = new Dictionary<Guid, Subscriber>();
_subscribersSnapshot = _subscribers.Values.ToEnumerableSnapshot();
// ...(elsewere)
foreach (var subscriber in _subscribersSnapshot)
{
//...
}
Unfortunately this idea cannot be used currently with the class Dictionary in .NET Core 3.0, because this class does not throw a Collection was modified exception when enumerated and the methods Remove and Clear are invoked. All other containers I checked are behaving consistently. I checked systematically these classes:
List<T>, Collection<T>, ObservableCollection<T>, HashSet<T>, SortedSet<T>, Dictionary<T,V> and SortedDictionary<T,V>. Only the two aforementioned methods of the Dictionary class in .NET Core are not invalidating the enumeration.
更新:通过比较缓存和原始集合的长度,我修复了上述问题。这个修正假设字典将直接作为参数传递给EnumerableSnapshot的构造函数,并且它的标识不会被(例如)像:dictionary这样的投影隐藏。选择(e => e).ΤοEnumerableSnapshot()。
重要:上面的类不是线程安全的。它旨在从专门运行在单个线程中的代码中使用。