不是线程安全的。 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>替代,它们从设计上就支持多订阅者与线程安全推送。
