Web API - ConfigureAwait(true) 没有像我想的那样工作

Web API - ConfigureAwait(true) not working as I thought

从 .NET v4.6 WebAPI 2 的角度来看,我在理解 "continueOnCapturedContext" 的来龙去脉时遇到了一些麻烦。

我遇到的问题是 ConfigureAwait(true) 和 ConfigureAwait(false) 之间似乎没有任何区别。

我整理了一个示例应用来演示正在发生的事情:

    public async Task<IHttpActionResult> Get(bool continueOnContext)
    {
        int beforeRunningExampleThreadId = Thread.CurrentThread.ManagedThreadId;
        int runningExampleThreadId = await ExecuteExampleAsync(continueOnContext).ConfigureAwait(continueOnContext);
        int afterRunningExampleThreadId = Thread.CurrentThread.ManagedThreadId;

        return Ok(new
        {
            HasSyncContext = SynchronizationContext.Current != null,
            ContinueOnCapturedContext = continueOnContext,
            BeforeRunningExampleThreadId = beforeRunningExampleThreadId,
            RunningExampleThreadId = runningExampleThreadId,
            AfterRunningExampleThreadId = afterRunningExampleThreadId,
            ResultingCulture = Thread.CurrentThread.CurrentCulture,
            SameThreadRunningAndAfter = runningExampleThreadId == afterRunningExampleThreadId
        });
    }

    private async Task<int> ExecuteExampleAsync(bool continueOnContext)
    {
        return await Task.Delay(TimeSpan.FromMilliseconds(10)).ContinueWith((task) => Thread.CurrentThread.ManagedThreadId).ConfigureAwait(continueOnContext);
    }

对于“/Test?continueOnContext=true”,这个 returns 我:

{"HasSyncContext":true,"ContinueOnCapturedContext":true,"BeforeRunningExampleThreadId":43,"RunningExampleThreadId":31,"AfterRunningExampleThreadId":56,"ResultingCulture":"fr-CA","SameThreadRunningAndAfter":false}

所以你可以看到我有一个 Sync 上下文,我正在执行 ConfigureAwait(true) 但线程不是 "continuing" - 之前分配了一个新线程,而 运行 和 运行 之后的异步代码。这不像我期望的那样工作 - 我在这里有一些基本的误解吗?

有人可以向我解释为什么在此代码中 ConfigureAwait(true) 和 ConfigureAwait(false) 有效地做同样的事情吗?

更新 - 我想出来了,并在下面回答了。我也喜欢来自的答案 @YuvalShap。如果你像我一样被困在这个问题上,我建议你都阅读。

好的,我明白了,所以我会 post 一个答案,以防对其他人有所帮助。

在 .NET 4.6 WebAPI 2 中 - 我们继续使用的 "Captured Context" 不是线程,而是请求上下文。除其他事项外,请求上下文了解 HttpContext。当指定 ConfigureAwait(true) 时,我们告诉 .NET 我们希望在等待之后保留请求上下文及其相关的所有内容(HttpContext 和其他一些属性)——我们希望 return 到我们的上下文开始于 - 这不考虑线程。

当我们指定 ConfigureAwait(false) 时,我们是说我们不需要 return 我们开始的请求上下文。这意味着 .NET 可以 return 返回,而不必关心 HttpContext 和其他一些属性,因此可以提高边际性能。

鉴于这些知识,我更改了我的代码:

    public async Task<IHttpActionResult> Get(bool continueOnContext)
    {
        var beforeRunningValue = HttpContext.Current != null;
        var whileRunningValue = await ExecuteExampleAsync(continueOnContext).ConfigureAwait(continueOnContext);
        var afterRunningValue = HttpContext.Current != null;

        return Ok(new
        {
            ContinueOnCapturedContext = continueOnContext,
            BeforeRunningValue = beforeRunningValue,
            WhileRunningValue = whileRunningValue,
            AfterRunningValue = afterRunningValue,
            SameBeforeAndAfter = beforeRunningValue == afterRunningValue
        });
    }

    private async Task<bool> ExecuteExampleAsync(bool continueOnContext)
    {
        return await Task.Delay(TimeSpan.FromMilliseconds(10)).ContinueWith((task) =>
        {
            var hasHttpContext = HttpContext.Current != null;
            return hasHttpContext;
        }).ConfigureAwait(continueOnContext);
    }

当 continueOnContext = true 时: {"ContinueOnCapturedContext":true,"BeforeRunningValue":true,"WhileRunningValue":false,"AfterRunningValue":true,"SameBeforeAndAfter":true}

当 continueOnContext = false 时: {"ContinueOnCapturedContext":false,"BeforeRunningValue":true,"WhileRunningValue":false,"AfterRunningValue":false,"SameBeforeAndAfter":false}

所以从这个例子中你可以看到 HttpContext.Current 在异步方法之前存在,并且在异步方法期间丢失,无论 ConfigureAwait 设置如何。

差异在异步操作完成后出现:

  • 当我们指定 ConfigureAwait(true) 时,我们会回到调用异步方法的请求上下文——这会做一些内务处理并同步 HttpContext,因此当我们继续时它不为空
  • 当我们指定 ConfigureAwait(false) 时,我们只是继续而不返回请求上下文,因此 HttpContext 为 null

When an asynchronous handler resumes execution on legacy ASP.NET, the continuation is queued to the request context. The continuation must wait for any other continuations that have already been queued (only one may run at a time). When it is ready to run, a thread is taken from the thread pool, enters the request context, and then resumes executing the handler. That “re-entering” the request context involves a number of housekeeping tasks, such as setting HttpContext.Current and the current thread’s identity and culture.

来自 ASP.NET Core SynchronizationContext Stephen Cleary 的博客 post。

综上所述,Core 之前的 ASP.NET 版本使用 AspNetSynchronizationContext 作为请求上下文,这意味着当您调用 ConfigureAwait(true)(或不调用 ConfigureAwait(false) ) 你捕获了告诉方法在请求上下文中恢复执行的上下文。 请求上下文保持 HttpContext.Current 和当前线程的身份和文化一致,但它不是特定线程独有的,唯一的限制是一次只有一个线程可以 运行 在上下文中。