返回首页

简介
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!

回答

评论会员:游客 时间:2012/01/25
地址最"线程安全"的解决方案不同的迭代只处理基本的CRUD安全
tonyf8888
评论会员:游客 时间:2012/01/25
喜阿列克谢,这个代码是在我的项目非常有帮助。非常感谢你发布这篇文章!我实现了一个解决方案,使用一个ReaderWriterLockSlim你的建议。我的单元测试抛出一个InvalidOperationException,一些进一步的调查使我相信的GetEnumerator()方法获取读锁之前创建的内在枚举的情况下,有一个等待写锁,这导致了写锁首先被授予内心枚举总是被修改无效之前,它有一个要使用的机会。我做了一个微小的调整,这个代码:codeprelang="xml"publicIEnumeratorspanclass="code-keyword"</spanspanclass="code-leadattribute"T/spanspanclass="code-keyword">/spanGetEnumerator(){returnnewSafeEnumeratorspanclass="code-keyword"</spanspanclass="code-leadattribute"T/spanspanclass="code-keyword">/span(m_Inner.GetEnumerator(),m_Lock);}//changedto:publicIEnumeratorspanclass="code-keyword"</spanspanclass="code-leadattribute"T/spanspanclass="code-keyword">/spanGetEnumerator(){_readerWriterLock.EnterReadLock();try{returnnewSafeEnumeratorspanclass="code-keyword"</spanspanclass="code-leadattribute"T/spanspanclass="code-keyword">/span(m_Inner.GetEnumerator(),_readerWriterLock);}finally{_readerWriterLock.ExitReadLock();}}/pre/code我相信这将确保集合不会在非常罕见的情况下,有一个写锁早已等候在此之前迭代得到一个机会,被包装SafeEnumerator的构造锁定它猛扑。更新一个重要的事情要注意的是,这个工作ReaderWriterLockSlim已启用这样的递归实例化:codeprespanclass="code-keyword"public/spanreadonlyReaderWriterLockSlim_readerWriterLock=spanclass="code-keyword"new/spanReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);/pre/code这种变化我的单元测试通过,似乎是有意义的我,但我知道,这东西是非常棘手。请检查出[imgsrc=http://www.orcode.com/img/ico/smiley_wink.gif您的想法和意见将非常欢迎
。jgauffin
评论会员:游客 时间:2012/01/25
既然你在Add方法,并在枚举器锁,怎么会是你的代码不同:名单myList的;//线程1锁(myList的)myList.Add(XXX);//线程2锁(myList的)FOREACH(.....){BR}
阿列克谢Drobyshevsky
评论会员:游客 时间:2012/01/25
不同的是,使用建议的技术锁在一个私人的对象。有没有需要明确锁定(的意义,你不能可能忘记锁定)
jgauffin
评论会员:游客 时间:2012/01/25
Ahhh。我的误解。我还以为你发现了一些更好的方法锁定(获得速度{S0}
乔什 - 菲舍尔
评论会员:游客 时间:2012/01/25
!现在这是我想读的文章的一种标识问题,提供了一个解决方案,并让我认为很好的工作。乔什-菲舍尔
supercat9
评论会员:游客 时间:2012/01/25
假设的clone()方法是线程安全的。即使它是,这种模式不能夸高性能-我们实际上是两次通过迭代整个集合,说分配了,然后克隆的内存垃圾收集。,而不是。Clone'ing集合,我赞成枚举列表(或类似的结构),然后枚举列表。收集只会被锁定而被列举的一个列表,一个操作需要有限的时间,不依赖于任何其它锁,不依赖于适当。Dispose'al。这种重复可能会增加执行时间和内存使用情况,但除非做列表项的操作是微不足道的藏品不存在于内存中,直到列举,这样的时间和内存使用情况将不会是一个重要的因素。在许多情况下,减少(和边界的!)当持有锁的时间将超过建设清单成本。唯一的情况下,我可以看到这种重复会失败得很惨,统计员,生产项目在内存中,只有当列举。试图从一个磁盘文件列举一百万条记录每2000字节可能实际没有包装,但会失败得很惨如果是"包裹"
乔什 - 菲舍尔
评论会员:游客 时间:2012/01/25
难道仅仅是我或者是这个出发触摸"脏读",并与数据库相关的其他问题呢?软件事务内存的神奇修复这一切呢?我喜欢你的建议,但我认为这是完全依赖于手头的问题,你提到的。我也觉得这是惊人的,我们生活在一个时代里的机器是如此之快和强大,我们甚至可以讨论复制整个集合,简单地列举的!{S1}乔什-菲舍尔
supercat9
评论会员:游客 时间:2012/01/25
乔什-菲舍尔写道:难道仅仅是我开始接触"脏读"等问题,并与数据库相关的吗?软件事务内存的神奇修复这一切呢?扩展条件写入自旋锁(*),似乎想一个实际的硬件解决方案,让弱的双比较和交换immplementation(**),解决许多问题,目前使用锁定。不幸的是,我不知道在任何主流架构的支持。(*)支持有条件写的处理器包括一个特殊的读写指令,失败的最后一个特殊的阅读的目标已被写入自读,写必须与语义,并应"通常是"成功没有;这种支持扩展版本允许使用两个特殊的读取和两个特殊的写入(**)一个完整的双比较和交换将存储"的"以'X','B'为'Y',只有'X'包含'A0'和'Y'包含'B0';否则它不会做任何事情。一个薄弱DCAS的行为将上面除了"X"可以写成'Y',即使不是"B0"("Y",将只得到书面如果两个条件都满足)。全部DCAS使得它很容易保持链表;有限DCAS的不漂亮,但它仍然使许多数据结构要维持简单的无锁算法比本来可能。乔什-菲舍尔写道:我喜欢你的建议,但我认为这是完全依赖于手头的问题;您提到。我也觉得这是惊人的,我们生活在一个时代里的机器是如此之快和强大,我们甚至可以讨论复制整个集合,简单地列举的!随着微软的IEnumerable的合同,有什么替代?我发现自己不知道,如果开发人员不应该写iEmumerable符合合同,我提供了另一篇文章,微软希望得到的提示
?kosat
评论会员:游客 时间:2012/01/25
"这种方法的问题是,锁定的对象是公共,任何人在任何地方都可以锁定,而这只是邀请死锁。"你是如何得出这样的结论呢?在这种情况下,显示器的目的,公共,让锁类的用户就可以,因为没有它的人做了一些疯狂的东西,像锁(collectioninstane){的foreach(在collectioninstaneXX);}这是"邀请死锁"imgsrc=http://www.orcode.com/img/ico/smiley_biggrin.gifimgsrc=http://www.orcode.com/img/ico/smiley_laugh.gif虽然,它的效率不高,锁定整个集合,并防止它由来自不同地方的安全红色...但是,其他的故事ReaderWriterLock或ReaderWriterLockSlim进场))顺便说一句读这个漂亮的文章imgsrc=http://www.orcode.com/img/ico/smiley_smile.gif
阿列克谢Drobyshevsky
评论会员:游客 时间:2012/01/25
我相信,任何可以使用一个类(或者一个小型组装)以外的锁被邀请死锁。当然,锁定集合本身甚至比它的SyncRoot锁定差,但的SyncRoot仍然是对我的口味太大众。关于ReaderWriterLock包装-其实我想写一个关于他们的文章,但被打(-8
supercat9
评论会员:游客 时间:2012/01/25
阿列克谢Drobyshevsky写道:当然,锁定集合本身甚至比其锁定的SyncRoot,但仍然是我的口味太的SyncRoot公开。对于锁定工作,所有的代码使用对象,使用相同的锁定。如果枚举集合的语义是如此不幸的设计任务持有锁,直到所有的元素都列举,我不能见人会如何处理,而不锁公共使用某些ReaderWriterLock味道会更好,如果所有的代码访问集合知道这样做,但有一些代码使用ReaderWriterLock而其他代码使用监视器将是灾难性的
阿列克谢Drobyshevsky
评论会员:游客 时间:2012/01/25
supercat9写道:我看不到人会如何处理,未做锁定公共这正是文章-创建一个枚举器,可以锁定私有对象。从线程安全的文章列表中的用户可以使用它没有任何明确锁定-一切照顾的实施,包括迭代使用foreach和LINQ那么,像模式codeprelang="cs"spanclass="code-keyword"if/span(!list.Contains(obj))list.Add(obj)/pre/code仍然不是线程安全的,需要一个公共的锁。如果我曾经制定一种方法,使这种无锁的线程安全的,我会写另一篇文章(-8
supercat9
评论会员:游客 时间:2012/01/25
阿列克谢Drobyshevsky写道:如果我设计一种方法,使这种无锁的线程安全的,我会写另一篇文章(-8那人似乎很容易。使用一个线程安全的字典,忽略的价值,然后:codepreTrytheDict.Add(Obj,Nothing)CatchExAsArgumentExceptionspanclass="code-string"'/spanspanclass="code-string"Guesstheobjectalreadyexistssodon'/spantadditEndTry/pre/code我想正常的字典API将允许一个指定添加应该如何工作的项目已经存在(可能使用"添加"和"新",每到一个枚举选择模式的重载版本,过载新会选择默认行为)。选择包括:MustNotExist"正常行为-重复抛出的异常IgnoreIfExists的方法将跳过添加如果对象已存在,并返回FALSE来表明它已完成如果该键已经存在,ReplaceIfExists的方法将取代现有项目与新的InsertDupeAhead"允许重复索引将产生新的项目,除非/直到它被删除;默认情况下,枚举将返回新项目的第一个InsertDupeBehind"允许重复索引将产生旧的项目,除非/直到它被删除;默认情况下,枚举将返回老项目的第一个顺便说一句,相同的选项,可支持其比较运算符列表,如果是比参考相等(默认的行为是InsertDupeBehind)其他
。supercat9
评论会员:游客 时间:2012/01/25
这似乎IEnumerable的原因,它要求很多头痛如果集合被修改,枚举失败。它似乎是一个更好的方法,将有一个合同,要求一个IEnumerable的工作足以满足一定的要求,即使修改集合:枚举可能只返回枚举开始后的一段时间,在收集的项目。必须一次返回枚举每一个项目在不断存在的集合,无需修改,因为枚举开始。最多一次是在枚举过程中插入或修改的项目必须为每个插入或修改这可能是一些容器样式遵守这样的合同(例如,如果在枚举过程中重新生成一个哈希表,它可能很难或不可能知道哪些项目已经返回的枚举),但对那些很难将切实遵守这样的合同,这似乎比规定一个愚蠢的例外(如果我有我的druthers,一个IEnumerable将抛出一个异常,只有当它未能遵守上述合约)更好的方法。即使一个IEnumerable使用锁,以满足在线程安全的时尚的上述合约,锁定修改项目或检索项目单独将远远低于锁定在整个枚举集合危险行为