【问题标题】:ExecutionContext does not flow up the call stack from async methodsExecutionContext 不会从异步方法沿调用堆栈向上流动
【发布时间】:2019-08-24 02:18:27
【问题描述】:

考虑以下代码:

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 只是一个常规方法.

但即使有了这样的理解,我仍然有一些问题:

  1. 这是错误/缺失功能还是有意的设计决定?
  2. 在编写依赖于AsyncLocal 的代码时,我是否需要担心这种行为?说,我想写我的TransactionScope-wannabe,它通过等待点传输一些环境数据。 AsyncLocal 够了吗?
  3. 当涉及到在整个“逻辑代码流”中保留值时,在 .NET 中是否有任何其他替代方案可以替代 AsyncLocal 和 CallContext.LogicalGetData / CallContext.LogicalSetData?

【问题讨论】:

    标签: c# .net async-await executioncontext


    【解决方案1】:

    这对我来说似乎是一个有意的决定。

    如您所知,SetValueInAsyncMethod 被编译为隐式捕获当前 ExecutionContext 的状态机。当您更改AsyncLocal-变量时,该更改不会“流”回调用函数。相反,SetValueInNonAsyncMethod 不是异步的,因此不会编译到状态机中。因此,不会捕获 ExecutionContext,并且调用者可以看到对 AsyncLocal-variables 的任何更改。

    如果出于任何原因需要,您也可以自己捕获 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,只要您不重新分配它。

    【讨论】:

      【解决方案2】:

      这是一个错误/缺失的功能还是有意的设计决定?

      这是一个有意的设计决定。具体来说,async 状态机为其逻辑上下文设置“写入时复制”标志。

      与此相关的是所有同步方法都属于它们最近的祖先async方法。

      在编写依赖于 AsyncLocal 的代码时,我是否需要担心这种行为?说,我想编写我的 TransactionScope-wannabe,它通过等待点来流动一些环境数据。这里的 AsyncLocal 够用吗?

      大多数类似这样的系统使用AsyncLocal&lt;T&gt; 结合IDisposable 模式,该模式清除AsyncLocal&lt;T&gt; 值。结合这些模式可确保它适用于同步或异步代码。如果消费代码是async 方法,AsyncLocal&lt;T&gt; 将自行正常工作;将它与 IDisposable 一起使用可确保它适用于 async 和同步方法。

      .NET 中的 AsyncLocal 和 CallContext.LogicalGetData / CallContext.LogicalSetData 是否有任何其他替代方法可以在整个“逻辑代码流”中保留值?

      没有。

      【讨论】:

      • 如果您不介意,请有人更详细地解释这句话:“将它与 IDisposable 一起使用可确保它适用于异步和同步方法”
      • @Ostati:从同步方法设置AsyncLocal&lt;T&gt;.Value 实际上会更改第一个 asynchronous 方法在调用堆栈上行进的值。因此,如果AAsync() 调用设置asyncLocal1.Value 的B(),那么AAsync() 将看到更新后的值。使用 IDisposable 为您提供了一个明确的范围,在该范围内 AsyncLocal&lt;T&gt;.Value 更新存在,并且在处置一次性用品时将其重置为之前的值。
      • 我还是不能完全理解这种情况。你能看看这个问题吗? stackoverflow.com/questions/66500737/…
      猜你喜欢
      • 1970-01-01
      • 2013-12-25
      • 1970-01-01
      • 2016-05-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-30
      • 2019-08-03
      相关资源
      最近更新 更多