yield return 迭代器不是线程安全的,因其生成的状态机类字段(如1__state和捕获变量)被多个线程并发读写而无同步机制,导致InvalidOperationException、元素跳过或重复等未定义行为。
不是线程安全的。 C# 中用 yield return 生成的迭代器(即实现了 IEnumerable 的方法)本身不提供任何线程同步机制,多个线程同时调用 GetEnumerator() 并遍历,或对同一个枚举器实例并发调用 MoveNext()/Current,会导致未定义行为,常见表现为 InvalidOperationException、跳过元素、重复返回、甚至死锁(取决于底层状态机实现)。
每个 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() 并并发消费核心原则是:**确保每个线程拥有独立的枚举器实例,并避免共享状态。** 常见做法包括:
IEnumerable 方法(即重新创建状态机实例),而不是复用枚举器.ToList() 或 .To
Array() 落实为线程安全的不可变集合(注意:这会失去延迟执行优势)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 替代,它们从设计上就支持多订阅者与线程安全推送。