使GetEnumerator线程安全

6

枚举器是如何工作的 - 我知道它们在幕后构建了一个状态机,但如果我两次调用GetEnumerator,我会得到两个不同的对象吗?

如果我做这样的事情

  public IEnumerator<T> GetEnumerator()
  {
     yield return 1;
     yield return 2;
  }

我能否在方法开始时获取锁,并且这个锁会一直保持到枚举器返回null或者枚举器被GC回收?

如果调用者重置了枚举器,会发生什么等问题 -

我的问题是,在处理枚举器时如何管理锁定的最佳方法

注意:客户端不能负责线程同步 - 类内部需要负责

最后,上面的示例是问题的简化 - yield语句做的比我展示的要多一些 :)


可能是C#:如何使IEnumerable<T>线程安全?的重复问题。 - steve cook
2个回答

10

是的,每次调用您的GetEnumerator()方法都会创建一个不同的对象(一个新的状态机)。

在迭代器块中,您可以获取锁,但要注意,在调用者第一次调用MoveNext()之前,您的方法中的任何代码都不会被调用。

一般而言,如果可能的话,我建议不要在迭代器块中保持锁定。您不知道调用者在调用MoveNext()之间会做什么。只要他们最终释放了迭代器,锁定最终就会被释放,但这仍然意味着您受到调用者的影响。

如果您能够提供更多关于您正在尝试做什么的信息,那将有所帮助。一种可能更容易正确实现的替代设计是:

public void DoSomething(Action<T> action)
{
    lock (...)
    {
        // Call action on each element in here
    }
}

我正在制作自己的数据结构,它需要是IEnumerable并且还需要是线程安全的。我已经保护了所有的读写操作,但是无法确定如何为枚举器进行线程锁定。 - Jack Kada
我能问一下吗 - 如果我在上面的方法入口处放置一个锁,并在所有yield return语句完成后释放锁定,那么锁定是否会在枚举期间保持?这可能就足够了。 - Jack Kada
@CycleMachine:首先,您需要定义“线程安全”的含义。例如,如果您的数据结构是不可变的,那么您就不需要任何锁定...但是,如果在迭代时隐式地锁定了所有内容,我会对这种设计持谨慎态度。 - Jon Skeet

2

正如John所说,很难给你一个好的答案。 显而易见的谷歌搜索链接是:http://www.codeproject.com/KB/cs/safe_enumerable.aspx

这背后的思想是在构造时锁定IEnumerable实例,这有很大的缺点。

接下来很明显的就是隔离,你可以创建一个结构的副本并迭代复制品。如果不加思考地实现,这会非常消耗内存,但如果你的数据集相对较小,则可能是值得的。

最好的方法是,如果你的数据是不可变的,那么你就自动具有线程安全性了,但如果你绑定到计数会发生更改的集合中,那么你就拥有了可变的数据结构。如果你能将你的数据结构重新设计为不可变的,那么问题便解决了。

由于长时间锁定数据并不是一个好主意,你可以实现一些策略来保证线程安全,当你利用确切的数据结构和用例时。例如,如果你很少更改数据并频繁枚举它,你可以实现一个乐观的枚举器,在枚举开始之前读取你的数据结构的写入计数器,并像往常一样产生结果。如果在读取期间发生了写入,你就可以抛出异常,以提示你的枚举器用户重试,直到他成功为止。虽然这种方法可行,但却将责任委派给了你的可枚举程序的调用者,要求他重试枚举,直到成功为止。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接