c# yield return 生成的迭代器是线程安全的吗

来源:这里教程网 时间:2026-02-21 17:38:42 作者:

不是线程安全的。 C# 中用

yield return
生成的迭代器(即实现了
IEnumerable<t></t>
的方法)本身不提供任何线程同步机制,多个线程同时调用
GetEnumerator()
并遍历,或对同一个枚举器实例并发调用
MoveNext()
/
Current
,会导致未定义行为,常见表现为
InvalidOperationException
、跳过元素、重复返回、甚至死锁(取决于底层状态机实现)。

为什么 yield return 迭代器不是线程安全的

每个

yield return
方法在编译后会生成一个隐藏的状态机类(如
<mymethod>d__0</mymethod>
),该类包含:

一个整型字段
1__state
,记录当前执行位置(-2=已结束,-1=未开始,0+为具体 yield 点)
所有被闭包捕获的局部变量作为字段存储
Current
属性和
MoveNext()
方法直接读写这些字段,无锁、无 volatile、无内存屏障
这意味着:只要两个线程操作的是同一个枚举器实例(
IEnumerator<t></t>
),就会竞争修改同一组字段。

哪些场景下容易出问题

以下情况极易触发线程安全问题:

多个线程共用同一个
IEnumerator<t></t>
实例(例如把
GetEnumerator()
结果存为字段后多线程调用
MoveNext()
在异步方法中(如
async Task<ienumerable>></ienumerable>
)返回
yield return
序列,但外部未 await 就直接枚举 —— 此时状态机可能跨线程切换,而枚举器本身仍无保护
yield return
方法的结果(
IEnumerable<t></t>
)传给并行 LINQ(如
AsParallel().Select(...)
),后者可能在多个线程中调用
GetEnumerator()
并并发消费

如何安全地在多线程中使用 yield return 序列

核心原则是:**确保每个线程拥有独立的枚举器实例,并避免共享状态。** 常见做法包括:

每次需要遍历时,都重新调用原始
IEnumerable<t></t>
方法(即重新创建状态机实例),而不是复用枚举器
若需缓存结果供多线程读取,先调用
.ToList()
.ToArray()
落实为线程安全的不可变集合(注意:这会失去延迟执行优势)
如必须保持延迟执行且支持并发访问,可手动包装一层线程安全的枚举逻辑(例如用
lock
同步
MoveNext()
Current
,但会严重损害性能,且违背
yield return
的设计初衷)
public static IEnumerable<int> Numbers()
{
    for (int i = 0; i < 10; i++)
    {
        yield return i;
    }
}
<p>// ✅ 安全:每个线程拿到新枚举器
var sharedSource = Numbers(); // IEnumerable<int>
Task.Run(() => { foreach (var x in sharedSource) Console.WriteLine(x); });
Task.Run(() => { foreach (var x in sharedSource) Console.WriteLine(x); });</p><p>// ❌ 危险:共享同一枚举器实例
var enumerator = Numbers().GetEnumerator();
Task.Run(() => { while (enumerator.MoveNext()) Console.WriteLine(enumerator.Current); });
Task.Run(() => { while (enumerator.MoveNext()) Console.WriteLine(enumerator.Current); }); // 可能抛 InvalidOperationException

真正需要并发消费的延迟序列,应考虑用

Channel<t></t>
IObservable<t></t>
替代,它们从设计上就支持多订阅者与线程安全推送。

相关推荐