ExecutionContext 不会从异步方法向上流动调用堆栈
ExecutionContext does not flow up the call stack from async methods
考虑以下代码:
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
并且SetValueInNonAsyncMethod
只是一个常规方法。
但即使有了这样的理解,我仍然有一些问题:
- 这是错误/缺失功能还是有意设计的决定?
- 在编写依赖于
AsyncLocal
的代码时,我是否需要担心这种行为?比如说,我想编写我的 TransactionScope
-wannabe,它通过等待点传输一些环境数据。 AsyncLocal
够吗?
- 当涉及到在整个 "logical code flow" 中保留值时,在 .NET 中是否有
AsyncLocal
和 CallContext.LogicalGetData
/ CallContext.LogicalSetData
的任何其他替代方案?
这对我来说似乎是一个有意的决定。
如您所知,SetValueInAsyncMethod
被编译到隐式捕获当前 ExecutionContext 的状态机中。当您更改 AsyncLocal
变量时,该更改不会 "flowed" 返回到调用函数。相反,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)中的值不会"flow back"。但是你当然仍然可以访问和修改 AsyncLocal
只要你不重新分配它。
Is this a bug / missing feature or an intentional design decision?
这是一个有意的设计决定。具体来说,async
状态机为其逻辑上下文设置 "copy on write" 标志。
与此相关的是所有 同步 方法都属于它们最近的祖先 async
方法。
Do I need to worry about this behavior while writing code that depends on AsyncLocal? Say, I want to write my TransactionScope-wannabe that flows some ambient data though await points. Is AsyncLocal enough here?
像这样的大多数系统使用 AsyncLocal<T>
结合 IDisposable
模式来清除 AsyncLocal<T>
值。组合这些模式可确保它适用于同步或异步代码。如果使用的代码是 async
方法,AsyncLocal<T>
本身就可以正常工作;将它与 IDisposable
一起使用可确保它适用于 async
和同步方法。
Are there any other alternatives to AsyncLocal and CallContext.LogicalGetData / CallContext.LogicalSetData in .NET when it comes down to preserving values throughout the "logical code flow"?
没有
考虑以下代码:
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
并且SetValueInNonAsyncMethod
只是一个常规方法。
但即使有了这样的理解,我仍然有一些问题:
- 这是错误/缺失功能还是有意设计的决定?
- 在编写依赖于
AsyncLocal
的代码时,我是否需要担心这种行为?比如说,我想编写我的TransactionScope
-wannabe,它通过等待点传输一些环境数据。AsyncLocal
够吗? - 当涉及到在整个 "logical code flow" 中保留值时,在 .NET 中是否有
AsyncLocal
和CallContext.LogicalGetData
/CallContext.LogicalSetData
的任何其他替代方案?
这对我来说似乎是一个有意的决定。
如您所知,SetValueInAsyncMethod
被编译到隐式捕获当前 ExecutionContext 的状态机中。当您更改 AsyncLocal
变量时,该更改不会 "flowed" 返回到调用函数。相反,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)中的值不会"flow back"。但是你当然仍然可以访问和修改 AsyncLocal
只要你不重新分配它。
Is this a bug / missing feature or an intentional design decision?
这是一个有意的设计决定。具体来说,async
状态机为其逻辑上下文设置 "copy on write" 标志。
与此相关的是所有 同步 方法都属于它们最近的祖先 async
方法。
Do I need to worry about this behavior while writing code that depends on AsyncLocal? Say, I want to write my TransactionScope-wannabe that flows some ambient data though await points. Is AsyncLocal enough here?
像这样的大多数系统使用 AsyncLocal<T>
结合 IDisposable
模式来清除 AsyncLocal<T>
值。组合这些模式可确保它适用于同步或异步代码。如果使用的代码是 async
方法,AsyncLocal<T>
本身就可以正常工作;将它与 IDisposable
一起使用可确保它适用于 async
和同步方法。
Are there any other alternatives to AsyncLocal and CallContext.LogicalGetData / CallContext.LogicalSetData in .NET when it comes down to preserving values throughout the "logical code flow"?
没有