yield return 迭代器不是线程安全的,因其生成的状态机类字段(如1__state和捕获变量)被多个线程并发读写而无同步机制,导致InvalidOperationException、元素跳过或重复等未定义行为。

不是线程安全的。 C# 中用 yield return 生成的迭代器(即实现了 IEnumerable 的方法)本身不提供任何线程同步机制,多个线程同时调用 GetEnumerator() 并遍历,或对同一个枚举器实例并发调用 MoveNext()/Current,会导致未定义行为,常见表现为 InvalidOperationException、跳过元素、重复返回、甚至死锁(取决于底层状态机实现)。
为什么 yield return 迭代器不是线程安全的
每个 yield return 方法在编译后会生成一个隐藏的状态机类(如 ),该类包含:
- 一个整型字段
1__state,记录当前执行位置(-2=已结束,-1=未开始,0+为具体 yield 点) - 所有被闭包捕获的局部变量作为字段存储
-
Current属性和MoveNext()方法直接读写这些字段,无锁、无 volatile、无内存屏障
IEnumerator),就会竞争修改同一组字段。哪些场景下容易出问题
以下情况极易触发线程安全问题:
- 多个线程共用同一个
IEnumerator实例(例如把GetEnumerator()结果存为字段后多线程调用MoveNext()) - 在异步方法中(如
async Task)返回> yield return序列,但外部未 await 就直接枚举 —— 此时状态机可能跨线程切换,而枚举器本身仍无保护 - 将
yield return方法的结果(IEnumerable)传给并行 LINQ(如AsParallel().Select(...)),后者可能在多个线程中调用GetEnumerator()并并发消费
如何安全地在多线程中使用 yield return 序列
核心原则是:**确保每个线程拥有独立的枚举器实例,并避免共享状态。** 常见做法包括:
- 每次需要遍历时,都重新调用原始
IEnumerable方法(即重新创建状态机实例),而不是复用枚举器 - 若需缓存结果供多线程读取,先调用
.ToList()或.ToArray()落实为线程安全的不可变集合(注意:这会失去延迟执行优势) - 如必须保持延迟执行且支持并发访问,可手动包装一层线程安全的枚举逻辑(例如用
lock同步MoveNext()和Current,但会严重损害性能,且违背yield return的设计初衷)
public static IEnumerableNumbers() { for (int i = 0; i < 10; i++) { yield return i; } } // ✅ 安全:每个线程拿到新枚举器 var sharedSource = Numbers(); // IEnumerable
Task.Run(() => { foreach (var x in sharedSource) Console.WriteLine(x); }); Task.Run(() => { foreach (var x in sharedSource) Console.WriteLine(x); }); // ❌ 危险:共享同一枚举器实例 var enumerator = Numbers().GetEnumerator(); Task.Run(() => { while (enumerator.MoveNext()) Console.WriteLine(enumerator.Current); }); Task.Run(() => { while (enumerator.MoveNext()) Console.WriteLine(enumerator.Current); }); // 可能抛 InvalidOperationException
真正需要并发消费的延迟序列,应考虑用 Channel 或 IObservable 替代,它们从设计上就支持多订阅者与线程安全推送。










