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

本文关键字:操作 迭代器 跟踪 创建 线程 为什么 | 更新日期: 2023-09-27 18:15:49

这是自从我在Jon Skeet的网站上读到迭代器后一直困扰我的问题。

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

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

通常,当我处于类似的位置时,我会使用我认为简单的Interlocked。CompareExchange -确保只有一个线程能够将状态从"available"更改为"in process"。

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

但是设计团队的方法?每个IEnumerable都保留了创建线程的托管线程ID的一个字段,然后那个线程ID在调用GetEnumerator时针对这个字段进行检查,只有当它是同一个线程,并且是它第一次被调用时,IEnumerable才能返回自己作为IEnumerator。在我看来,这似乎更难解释。

我只是想知道为什么要采取这种方法。互锁操作比两次调用System.Threading.Thread.CurrentThread要慢得多。ManagedThreadId,如此之多,以至于它证明了额外的字段?

或者这背后有其他原因,可能涉及内存模型或ARM设备或我没有看到的东西?也许规范对IEnumerable的实现有特定的要求?

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

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

是互锁操作,远比两次调用System.Threading.Thread.CurrentThread。ManagedThreadId,以至于它证明了额外的字段是合理的?

是的,联锁操作比两次调用ManagedThreadId要慢得多——联锁操作并不便宜,因为它们需要多cpu系统来同步它们的缓存。

来自理解低锁技术对多线程应用程序的影响:

互锁指令需要确保缓存同步所以读和写似乎没有越过指令。这取决于内存系统的细节和内存的大小最近在各种处理器上进行了修改,这可能非常昂贵(数百个指令周期)。

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

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

  1. 使用一次
  2. 在创建它的同一个线程上使用
  3. 短暂的

因此,这种设计不会带来同步开销,并牺牲4字节,这可能只会在很短的时间内使用。

当然,要证明这一点,您必须进行性能分析以确定相对成本,并进行代码分析以证明常见情况是什么。