考虑以下代码:
private static async Task Main(string[] args)
{
await SetValueInAsyncMethod();
PrintValue();
await SetValueInNonAsyncMethod();
PrintValue();
}
private static readonly AsyncLocal<int> asyncLocal = new AsyncLocal<int>();
private static void PrintValue([CallerMemberName] string callingMemberName = "")
{
Console.WriteLine($"{callingMemberName}: {asyncLocal.Value}");
}
private static async Task SetValueInAsyncMethod()
{
asyncLocal.Value = 1;
PrintValue();
await Task.CompletedTask;
}
private static Task SetValueInNonAsyncMethod()
{
asyncLocal.Value = 2;
PrintValue();
return Task.CompletedTask;
}
如果在.NET 4.7.2控制台应用程序中运行此代码,则会得到以下输出:
SetValueInAsyncMethod: 1
Main: 0
SetValueInNonAsyncMethod: 2
Main: 2
我确实了解到,输出的差异是由于SetValueInAsyncMethod
并不是真正的方法,而是由AsyncTaskMethodBuilder
执行的状态机在内部捕获ExecutionContext
和{{ 1}}只是一种常规方法。
但是即使有这种理解,我仍然有一些疑问:
SetValueInNonAsyncMethod
的代码时,我是否需要担心这种行为?说,我想写我的AsyncLocal
-想让一些环境数据流过等待点。这里TransactionScope
够了吗?AsyncLocal
和AsyncLocal
/ CallContext.LogicalGetData
是否还有其他替代方法?答案 0 :(得分:4)
对我来说,这似乎是一个故意的决定。
您已经知道,SetValueInAsyncMethod
被编译成一个状态机,该状态机隐式捕获了当前的ExecutionContext。当您更改AsyncLocal
变量时,该更改不会“流”回调用函数。相反,SetValueInNonAsyncMethod
不是异步的,因此不会编译为状态机。因此,不会捕获ExecutionContext,并且AsyncLocal
变量的任何更改对调用者都是可见的。
如果出于任何原因需要这样做,您也可以自己捕获ExecutionContext:
private static Task SetValueInNonAsyncMethodWithEC()
{
var ec = ExecutionContext.Capture(); // Capture current context into ec
ExecutionContext.Run(ec, _ => // Use ec to run the lambda
{
asyncLocal.Value = 3;
PrintValue();
});
return Task.CompletedTask;
}
这将输出3,而Main将输出2。
当然,简单地将SetValueInNonAsyncMethod
转换为异步以使编译器为您执行此操作更容易。
对于使用AsyncLocal
(或CallContext.LogicalGetData
的代码)来说,重要的是要知道,在调用的异步方法(或任何捕获的ExecutionContext)中更改值不会“背部”。但是,当然,只要不重新分配AsyncLocal
,您仍然可以访问和修改。
答案 1 :(得分:4)
这是错误/缺少功能还是故意的设计决定?
这是一个故意的设计决定。具体来说,async
状态机为其逻辑上下文设置“写时复制”标志。
与此相关的是,所有 synchronous 方法都属于其最接近的祖先async
方法。
编写依赖于AsyncLocal的代码时,我是否需要担心这种行为?说,我想编写我的TransactionScope-想通过等待点流一些环境数据。这里的AsyncLocal是否足够?
大多数类似这样的系统都使用AsyncLocal<T>
和IDisposable
模式来清除AsyncLocal<T>
值。组合这些模式可确保它与同步或异步代码一起使用。如果使用的代码是AsyncLocal<T>
方法,则async
本身可以很好地工作;将其与IDisposable
配合使用可确保它可以与async
和同步方法一起使用。
.NET中的AsyncLocal和CallContext.LogicalGetData / CallContext.LogicalSetData是否还有其他替代方法,可归结为在整个“逻辑代码流”中保留值?
否。