IAsyncEnumerable迭代点间AsyncLocal值丢失原因及解决方法
AsyncLocal 在异步枚举中的异常行为问题
await foreach (var item in Wrap(Sequence())) continue; static IAsyncEnumerable<int> Sequence() { return Core(); static async IAsyncEnumerable<int> Core([EnumeratorCancellation] CancellationToken ct = default) { yield return 123; await Task.Delay(TimeSpan.FromSeconds(5), ct); yield return 456; } } static IAsyncEnumerable<int> Wrap(IAsyncEnumerable<int> source) { return Core(source); static async IAsyncEnumerable<int> Core(IAsyncEnumerable<int> source, [EnumeratorCancellation] CancellationToken ct = default) { Test.Context.Value = "Hello world"; await foreach (var value in source.WithCancellation(ct)) { var before = Test.Context.Value; // 预期始终是"Hello world" yield return value; var after = Test.Context.Value; // 预期始终是"Hello world" Console.WriteLine($"Before={before}, after={after}"); } } } static class Test { public static readonly AsyncLocal<string> Context = new(); }
预期捕获到的before和after值都应为"Hello world",但实际结果:
- 第一次迭代:
before为"Hello world",after为null - 第二次迭代:
before和after均为null
1. 问题原因分析
这是因为AsyncLocal的上下文绑定是和当前执行的异步上下文关联的,而异步枚举的状态机在yield return后会将控制权交还给调用方:
- 当
Wrap.Core方法中设置Test.Context.Value = "Hello world"后,进入第一次await foreach迭代,此时执行上下文包含这个AsyncLocal值,所以before能正确获取到。 - 执行
yield return value后,状态机暂停,控制权回到最外层的await foreach调用方,此时调用方的异步上下文并没有设置过Test.Context.Value,值为null。 - 当第一次迭代结束,状态机恢复执行(回到
yield return之后的代码),此时延续的是调用方的异步上下文,所以after的值变成了null。 - 第二次迭代时,整个执行上下文已经是调用方的上下文,
Test.Context.Value始终为null,因此before和after都取不到预期值。
简单来说:异步枚举的每次yield都会让执行上下文切换回调用方,而调用方的上下文没有保留你在Wrap中设置的AsyncLocal值,导致后续代码执行时上下文丢失。
2. 修复方案:确保AsyncLocal上下文在迭代中保留
要让AsyncLocal的值在整个枚举过程中保留,需要在每次状态机恢复执行时,重新设置AsyncLocal的值。可以通过以下方式修改Wrap.Core方法:
static IAsyncEnumerable<int> Wrap(IAsyncEnumerable<int> source) { return Core(source); static async IAsyncEnumerable<int> Core(IAsyncEnumerable<int> source, [EnumeratorCancellation] CancellationToken ct = default) { var contextValue = "Hello world"; await foreach (var value in source.WithCancellation(ct)) { // 每次迭代开始时重置AsyncLocal值 Test.Context.Value = contextValue; var before = Test.Context.Value; yield return value; // yield返回后,重新设置AsyncLocal值,确保后续代码使用正确上下文 Test.Context.Value = contextValue; var after = Test.Context.Value; Console.WriteLine($"Before={before}, after={after}"); } } }
修复逻辑说明:
- 将目标上下文值存入本地变量
contextValue,避免依赖AsyncLocal的存储。 - 在每次迭代的开始(
yield return前)和结束后(yield return后),都显式设置Test.Context.Value = contextValue,确保无论执行上下文如何切换,当前代码块内的AsyncLocal值始终符合预期。 - 这种方式强制在
Wrap的每个执行阶段都覆盖AsyncLocal的值,不受调用方上下文的影响。
内容的提问来源于stack exchange,提问作者Bogey
相关产品推荐
相关产品推荐

