ExecutionContext不会从异步方法中累积调用堆栈

时间:2019-04-02 17:06:50

标签: c# .net async-await executioncontext

考虑以下代码:

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}}只是一种常规方法。

但是即使有这种理解,我仍然有一些疑问:

  1. 这是错误/缺少功能还是故意的设计决定?
  2. 编写依赖于SetValueInNonAsyncMethod的代码时,我是否需要担心这种行为?说,我想写我的AsyncLocal-想让一些环境数据流过等待点。这里TransactionScope够了吗?
  3. .NET在整个“逻辑代码流”中保留值时,.NET中的AsyncLocalAsyncLocal / CallContext.LogicalGetData是否还有其他替代方法?

2 个答案:

答案 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是否还有其他替代方法,可归结为在整个“逻辑代码流”中保留值?

否。