我目前正在实现一个观察者设计的情况。
我在我的主类中有我的注册观察者列表。
private static volatile List<IObserver> registeredObservers;
每个观察者都是一个网络套接字,当套接字连接好并且一切正常时,它将自己注册到可观察类中。如果出现错误或套接字只是断开连接,则从可观察对象中注销/删除其自身。
这一切似乎都工作得很好。我很高兴。
但是现在在我的主类中,我循环遍历所有注册的观察者,如下所示:
private void SendEventToObservers(ILogItem item)
{
foreach (var observer in registeredObservers)
{
if (observer != null)
{
observer.OnMessageRecieveEvent(new ObserverEvent(item));
}
}
}
然后抛出以下错误:
Collection was modified; enumeration operation may not execute
现在我已经在另一个SO线程中读到,我需要更改每个线程以添加。tolist ():
foreach (var observer in registeredObservers.ToList())
现在这并不能完全解决我的问题,因为如果观察者在忙于循环时从列表中删除自己,那么.toList()会创建列表的"旧"表示?
我在想,会不会有一种更好的并发列表,在被访问时可以阻塞,当观察者被添加和删除时?
或者当前的。tolist()解决方案是否有效?
既然你已经提到它是OK的block when being accessed and when observers are being added and removed
,我认为同步线程是最安全的选择。
如果您不运行时间密集型操作,这将是最佳解决方案。只需lock
列表并执行操作:
添加:
lock (registeredObservers)
registeredObservers.Add(newObserver);
删除:
lock (registeredObservers)
registeredObservers.Remove(outOfServiceObserver);
迭代:
lock (registeredObservers)
foreach (var observer in registeredObservers)
{
if (observer != null)
{
observer.OnMessageRecieveEvent(new ObserverEvent(item));
}
}
更新:
@fourpastmidnight对这个解决方案提出了一个严重的问题,我认为最好将答案编辑为更安全的东西:迭代列表的副本
lock (registeredObservers)
foreach (var observer in registeredObservers.ToArray())
{
if (observer != null)
{
observer.OnMessageRecieveEvent(new ObserverEvent(item));
}
}
当使用foreach
循环时,它使用枚举器,您不能修改被枚举的集合的内容。
所以看起来像是在枚举套接字集合时,一个套接字断开连接并从列表中删除(或者打开一个新的套接字并将其添加到列表中)。由于您的列表是在多线程场景中使用的,因此在枚举列表时就会发生这种情况,并且会发生异常。MSDN文档非常清楚地指出,标准的。net集合不是线程安全的。
您需要使用位于System.Collections.Concurrent
名称空间中的线程安全集合,例如System.Collections.Concurrent.ConcurrentBag<T>
或System.Collections.Concurrent.BlockingCollection<T>
,具体取决于您的需要。