为什么c#迭代器使用互锁操作跟踪创建线程?

时间:2011-11-28 04:48:13

标签: c# iterator

自从我读到Jon Skeet的site上的迭代器以来,这一直是令我困惑的事情。

Microsoft使用自动迭代器实现了一个简单的性能优化 - 返回的IEnumerable可以作为IEnumerator重用,从而节省了对象创建。现在因为IEnumerator必然需要跟踪状态,所以这只有在第一次迭代时才有效。

我无法理解的是为什么设计团队采取他们所采取的方法来确保线程安全。

通常当我处于类似的位置时,我会使用我认为是简单的Interlocked.CompareExchange - 以确保只有一个线程能够将状态从“可用”更改为“正在进行中”。

从概念上讲,它非常简单,只需一个原子操作,不需要额外的字段等。

但设计团队接近了吗?每个IEnumerable都保留一个创建线程的托管线程ID的字段,然后在针对该字段调用GetEnumerator时检查该线程ID,并且只有当它是相同的线程时,并且它是第一次被调用时,IEnumerable才能返回自身作为IEnumerator。似乎很难推理,imo。

我只是想知道为什么采用这种方法。 Interlocked操作是否比对System.Threading.Thread.CurrentThread.ManagedThreadId的两次调用慢得多,以至于它证明了额外字段的合理性?

或者还有其他原因,可能涉及内存模型或ARM设备或者我没有看到的东西?也许规范赋予了IEnumerable实现的特定要求?只是真的很困惑。

1 个答案:

答案 0 :(得分:2)

我不能肯定地回答,但关于你的问题:

  

Interlocked操作比两次调用慢得多   System.Threading.Thread.CurrentThread.ManagedThreadId,以至于那么多   它证明额外的领域是合理的吗?

是的,两次调用ManagedThreadId - 联锁操作的互锁操作要慢得多,因为它们需要多CPU系统来同步它们的缓存。

来自Understanding the Impact of Low-Lock Techniques in Multithreaded Apps

  

互锁指令需要确保缓存是同步的   因此,读取和写入似乎不会超出指令。   取决于内存系统的详细信息和内存量   最近在各种处理器上进行了修改,这可能相当昂贵   (数百个指令周期)。

Threading in C#中,它将开销列为10ns。获取ManagedThreadId应该是静态数据的正常非锁定读取。

现在这只是我的推测,但是如果你考虑正常的用例,那就是调用函数来检索IEnumerable并立即迭代它一次。所以在标准用例中,对象是:

  1. 使用一次
  2. 在创建它的同一个线程上使用
  3. 短暂的
  4. 因此,这种设计不会产生同步开销,并且会牺牲4个字节,这可能只会在很短的时间内使用。

    当然要证明这一点,你必须进行性能分析,以确定相对成本和代码分析,以证明常见的情况。