简介
IEnumerable接口在C#是一个非常有用的抽象。 NET 3.0中的LINQ简介使得它更加灵活。然而,在多线程环境中,使用它是造成了危险,可能随时修改底层集合,立即销毁您的foreach循环或LINQ表达式。
我要建议一个简单的一招,这将使超级简单的创建线程安全的统计员战略性使用另一个伟大的接口:IDisposable的。与迭代问题
作为MSDN上正确地指出,"通过列举一个集合在本质上不是一个线程安全的的procedurequot;即使您使用一个同步的集合(或之一。NET 4.0中的并发集合),和所有的方法在内部使用lock语句,迭代使用foreach仍可能失败。另一个线程可以改变的集合,当控制foreach循环内 - 因此,集合不锁定 - 和BAM,InvalidOperationException异常!
传统上,这个问题就迎刃而解了包裹在这样的lock语句的循环:lock(collection.SyncRoot){
foreach(var item in collection){
// do stuff
}
}
这种方法的问题是,锁定的对象是公共的,任何人在任何地方都可以锁定。而且,这只是邀请死锁。
在一个线程安全的的方式迭代的另一种方式是简单地使一个集合的副本:{C}
假设的clone()方法是线程安全的。即使它是,这种模式不能夸高性能 - 我们实际上是两次通过迭代整个集合,说任何分配和垃圾收集克隆的记忆。
当然,一个更好的方法将写的东西,如:foreach(var item in collection.ThreadSafeEnumerator()){
// do stuff
}
,它会自动在第一个例子一样线程安全的。这是如何实现这一目标。创建一个线程安全的枚举
一个伟大的事情有关的foreach(,推而广之,LINQ),是正确执行的Dispose()方法的枚举。也就是说,FOREACH实际上变成一个try - finally语句,创建枚举,然后一个try块内迭代,并在最终处置。 IEnumeratorlt; TGT接口实际上是从IDisposable继承其非泛型对应的是没有(因为在NET 1.0中的foreach没有这样的工作。)
把握这个优势,我们将进入一个锁的构造,并退出在Dispose()创建一个枚举。通过这种方式,集合将保持锁定整个迭代,并没有流氓线程就能够改变它。public class SafeEnumerator<T>: IEnumerator<T>
{
// this is the (thread-unsafe)
// enumerator of the underlying collection
private readonly IEnumerator<T> m_Inner;
// this is the object we shall lock on.
private readonly object m_Lock;
public SafeEnumerator(IEnumerator<T> inner, object @lock)
{
m_Inner = inner;
m_Lock = @lock;
// entering lock in constructor
Monitor.Enter(m_Lock);
}
#region Implementation of IDisposable
public void Dispose()
{
// .. and exiting lock on Dispose()
// This will be called when foreach loop finishes
Monitor.Exit(m_Lock);
}
#endregion
#region Implementation of IEnumerator
// we just delegate actual implementation
// to the inner enumerator, that actually iterates
// over some collection
public bool MoveNext()
{
return m_Inner.MoveNext();
}
public void Reset()
{
m_Inner.Reset();
}
public T Current
{
get { return m_Inner.Current; }
}
object IEnumerator.Current
{
get { return Current; }
}
#endregion
}
这是一个简单的枚举,进入创建和出售退出的锁。它的使用,我们必须创建一个集合,使用它。例如:public class MyList<T>: IList<T>{
// the (thread-unsafe) collection that actually stores everything
private List<T> m_Inner;
// this is the object we shall lock on.
private readonly object m_Lock=new object();
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
// instead of returning an usafe enumerator,
// we wrap it into our thread-safe class
return new SafeEnumerator<T>(m_Inner.GetEnumerator(), m_Lock);
}
// To be actually thread-safe, our collection
// must be locked on all other operations
// For example, this is how Add() method should look
public void Add(T item)
{
lock(m_Lock)
m_Inner.Add(item);
}
// ... the rest of IList<T> implementation goes here
}
这个例子显示了一个线程安全的包装,周围Listlt; TGT;此包装是绝对同步 - 在foreach循环中使用时,没有其他线程可以用它做任何事情。其他整齐的东西
周围Listlt编写线程安全包装; TGT(或任何其他集合)是有用的,但我们可以做得更好。让我们使用扩展方法!首先,这里的周围IEnumerablelt包装; TGT;public class SafeEnumerable<T> : IEnumerable<T>
{
private readonly IEnumerable<T> m_Inner;
private readonly object m_Lock;
public SafeEnumerable(IEnumerable<T> inner, object @lock)
{
m_Lock = @lock;
m_Inner = inner;
}
#region Implementation of IEnumerable
public IEnumerator<T> GetEnumerator()
{
return new SafeEnumerator<T>(m_Inner.GetEnumerator(), m_Lock);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
}
使用这种包装,我们可以写一个扩展的所有枚举集合:public static class EnumerableExtension
{
public static IEnumerable<T> AsLocked<T>(this IEnumerable<T> ie, object @lock)
{
return new SafeEnumerable<T>(ie, @lock);
}
}
现在,我们可以锁定任何集合,通过简单地使用这种方法:// in a class...
public class MyThreadSafeEnumerable<T>{
// come collection of items..
private IEnumerable<T> m_Items;
private readonly object m_Lock=new object();
// and thread-safe getter for them!
public IEnumerable<T> Items{
get
{
return m_Items.AsLocked(m_Lock);
}
}
}
// .. or simply in loop
foreach(var item in someList.AsLocked(someLock)){
// ...
}
整洁,是吧?当然,这最后一个例子是事实上,从本文开头相同的锁定方法。尽管如此,它可以说是更具可读性。而且,当锁是私人的,它甚至更具可读性和减少死锁倾向。其他注意事项
,虽然它的所有良好使用foreach时,使用此枚举显式调用collection.GetEnumerator()是比以前更危险。忘记调用Dispose(),和你的收藏是永远停留在锁。实现枚举定稿模式可能帮助它,但真的,要走的路,是永远不会使用,除非绝对必要的GetEnumerator()。真的,不使用它即使在当时。
此外,必须指出的是,即使是私人的锁定对象不保证无死锁的代码。作为一个foreach循环内的代码可能是任意的,仍然可以管理死锁。举例来说,像这样:// thread 1:
foreach(var item in SafeCollection){
// do stuff
lock(SomeObject){
// do other stuff
}
}
// thread 2:
lock(SomeObject){
// do stuff
SafeCollection.Add(foo); // <-Deadlock!
}
在这里,第一个线程锁定集合,然后尝试进入SomeObject锁..这是举行的第二个线程,并等待集合的锁添加到它的东西。因此,永远不会释放锁,线程挂断。死锁是这样的棘手。为了纠正这种情况,您可以添加超时Monitor.Enter(在构造函数中),并抛出一个异常,如果超时过期。仍是不是程序有一个正确的行为异常,但它无疑比死锁更好 - 当然更容易调试!
线程安全的枚举另一种可能升级到是使用ReaderWriterLock在监视器的地方(甚至更好,ReaderWriterLockSlim)。为迭代不改变集合,是有意义的,让许多并发迭代一次,只有块并发集合。这是什么ReaderWriterLock!