ExecutionContext 和 SynchronizationContext

使用 asyncawait处理时,两种上下文类型具有重要的但非常不同的角色: ExecutionContextSynchronizationContext。 你将了解每个元素的作用,每个元素如何与 async/await 交互,以及为什么 SynchronizationContext.Current 不在等待点之间流动。

什么是 ExecutionContext?

ExecutionContext 是一个用于传递程序逻辑控制流中环境状态的容器。 在同步世界中,环境信息位于线程本地存储(TLS)中,给定线程上运行的所有代码都可以看到该数据。 在异步世界中,逻辑操作可以在一个线程上启动、挂起和恢复在不同的线程上。 线程本地数据不会自动传递 - ExecutionContext 使其能够传递。

ExecutionContext 的流动方式

使用 ExecutionContext.Capture() 捕获 ExecutionContext。 在执行委托期间使用 ExecutionContext.Run 还原它:

static void ExecutionContextCaptureDemo()
{
    // Capture the current ExecutionContext
    ExecutionContext? ec = ExecutionContext.Capture();

    // Later, run a delegate within that captured context
    if (ec is not null)
    {
        ExecutionContext.Run(ec, _ =>
        {
            // Code here sees the ambient state from the point of capture
            Console.WriteLine("Running inside captured ExecutionContext.");
        }, null);
    }
}
Sub ExecutionContextCaptureExample()
    ' Capture the current ExecutionContext
    Dim ec As ExecutionContext = ExecutionContext.Capture()

    ' Later, run a delegate within that captured context
    If ec IsNot Nothing Then
        ExecutionContext.Run(ec,
            Sub(state)
                ' Code here sees the ambient state from the point of capture
                Console.WriteLine("Running inside captured ExecutionContext.")
            End Sub, Nothing)
    End If
End Sub

在 .NET 中进行分叉工作的所有异步 API(RunQueueUserWorkItemBeginRead 等)都会捕获ExecutionContext,并在调用回调时使用存储的上下文。 在一个线程上捕获状态并在另一个线程上还原状态的过程就是“流动的执行上下文”的含义。

什么是 SynchronizationContext?

SynchronizationContext 是一个抽象,表示要在其中运行工作的目标环境。 不同的 UI 框架提供自己的实现:

  • Windows 窗体提供 WindowsFormsSynchronizationContext,它覆盖 Post 以调用 Control.BeginInvoke
  • WPF 提供 DispatcherSynchronizationContext,它覆盖 Post 以调用 Dispatcher.BeginInvoke
  • ASP.NET(在 .NET Framework 上)提供了其自己的上下文,以确保HttpContext.Current可用。

通过使用 SynchronizationContext 替代特定于框架的封送 API,可以编写能够跨 UI 框架工作的组件。

static class SyncContextExample
{
    public static void DoWork()
    {
        // Capture the current SynchronizationContext
        SynchronizationContext? sc = SynchronizationContext.Current;

        ThreadPool.QueueUserWorkItem(_ =>
        {
            // ... do work on the ThreadPool ...

            if (sc is not null)
            {
                sc.Post(_ =>
                {
                    // This runs on the original context (e.g. UI thread)
                    Console.WriteLine("Back on the original context.");
                }, null);
            }
        });
    }
}
Class SyncContextExample
    Public Shared Sub DoWork()
        ' Install a custom SynchronizationContext for demonstration
        Dim customContext As New SimpleSynchronizationContext()
        SynchronizationContext.SetSynchronizationContext(customContext)

        ' Capture the current SynchronizationContext
        Dim sc As SynchronizationContext = SynchronizationContext.Current

        ThreadPool.QueueUserWorkItem(
            Sub(state)
                ' ... do work on the ThreadPool ...

                If sc IsNot Nothing Then
                    sc.Post(
                        Sub(s)
                            ' This runs on the original context (e.g. UI thread)
                            Console.WriteLine("Back on the original context.")
                        End Sub, Nothing)
                Else
                    Console.WriteLine("No SynchronizationContext was captured.")
                End If
            End Sub)
    End Sub
End Class

' A minimal SynchronizationContext for demonstration purposes
Class SimpleSynchronizationContext
    Inherits SynchronizationContext

    Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
        ' Queue the callback to run on a thread pool thread
        ThreadPool.QueueUserWorkItem(
            Sub(s)
                d(state)
            End Sub)
    End Sub
End Class

捕获 SynchronizationContext

当你捕获 SynchronizationContext 时,会从 SynchronizationContext.Current 中读取引用并将其存储以供以后使用。 然后,可以在捕获的引用上调用 Post,将工作安排回该环境。

流式 ExecutionContext 与使用 SynchronizationContext

尽管这两种机制都涉及从线程捕获状态,但它们提供不同的用途:

  • 流式 ExecutionContext 意味着在代理执行期间捕获环境状态并使该相同状态成为当前状态。 委托在它最终运行的地方运行,状态跟随它。
  • 使用 SynchronizationContext 意味着捕获一个计划目标,并使用它来决定委托在哪里执行。 捕获的上下文控制委托的运行位置。

简言之: ExecutionContext 回答“应该显示什么环境?”,而 SynchronizationContext 回答“代码应在哪里运行?”

async/await 如何与两个不同的上下文交互

基础结构 async/await 会自动与两个上下文交互,但以不同的方式进行交互。

ExecutionContext 始终在流动

每当 await 挂起一个方法时(因为 awaiter 的 IsCompleted 返回 false),基础结构就会捕获 ExecutionContext。 当方法恢复时,延续在捕获的上下文中运行。 此行为被内置于异步方法生成器中(例如 AsyncTaskMethodBuilder),并且无论使用何种类型的可等待对象,都会适用。

SuppressFlow() 存在,但它不是像 ConfigureAwait(false) 这样的等待特定开关。 当抑制处于活动状态时,它会抑制对排队工作的 ExecutionContext 捕获。 它没有提供一个按 await 编程模型选项,告诉异步方法构建器跳过延续捕获的 ExecutionContext。 这种设计是有意的,因为 ExecutionContext 基础结构级支持在异步世界中模拟线程本地语义,大多数开发人员从不需要考虑它。

任务 awaiter 捕获 SynchronizationContext

TaskTask<TResult> 的 awaiter 包括对 SynchronizationContext 的支持。 异步方法生成器不包括此支持。

await 任务时:

  1. awaiter 检查 SynchronizationContext.Current
  2. 如果存在上下文,则 awaiter 会捕获它。
  3. 任务完成后,延续将被发布回捕获的上下文,而不是在完成的线程或线程池上运行。

此行为是“带你回到你所处的位置”的方式 await 。 例如,在桌面应用程序的 UI 线程上恢复。

ConfigureAwait 控制 SynchronizationContext 捕获

如果不希望出现封送处理行为,请使用 false 调用 ConfigureAwait

await task.ConfigureAwait(false);

当将 continueOnCapturedContext 设置为 false 时,awaiter 不会检查 SynchronizationContext,并且延续会在任务完成的任何地方运行(通常在线程池线程上)。 库作者应该在每次等待时使用 ConfigureAwait(false),除非代码特别需要在捕获的上下文中恢复。

SynchronizationContext.Current 在等待点之间流动

这一点最为重要:SynchronizationContext.Current不会在等待点之间流动。 运行时中的异步方法生成器使用内部重载来显式阻止SynchronizationContext作为ExecutionContext的一部分进行传递。

为什么这很重要

从技术上说,SynchronizationContextExecutionContext 可以包含的子上下文之一。 如果它作为 ExecutionContext 的一部分流动,在线程池线程上执行的代码可能会将 UI SynchronizationContext 视为 Current,这不是因为该线程是 UI 线程,而是因为上下文通过流“泄漏”。 这种变化将把SynchronizationContext.Current的含义从“我当前的环境”改变为“历史上存在于调用链中某个地方的环境”。

Task.Run 示例

请考虑将工作卸载到线程池的代码。 此处所述的 UI 线程行为仅在 SynchronizationContext.Current 非 null 的情况下适用,例如在 UI 应用中:

static class TaskRunExample
{
    public static async Task ProcessOnUIThread()
    {
        // This method is called from a thread with a SynchronizationContext.
        // Task.Run offloads work to the thread pool.
        string result = await Task.Run(async () =>
        {
            string data = await DownloadAsync();
            // Compute runs on the thread pool, not the original context,
            // because SynchronizationContext doesn't flow into Task.Run.
            return Compute(data);
        });

        // Back on the original context (the continuation is posted back).
        Console.WriteLine(result);
    }

    private static async Task<string> DownloadAsync()
    {
        await Task.Delay(100);
        return "downloaded data";
    }

    private static string Compute(string data) =>
        $"Computed: {data.Length} chars";
}
Class TaskRunExampleClass
    Public Shared Async Function ProcessOnUIThread() As Task
        ' If a SynchronizationContext is present when this method starts,
        ' the outer await captures it. Task.Run still offloads work to the thread pool.
        Dim result As String = Await Task.Run(
            Async Function()
                Dim data As String = Await DownloadAsync()
                ' Compute runs on the thread pool, not the caller's context,
                ' because SynchronizationContext doesn't flow into Task.Run.
                Return Compute(data)
            End Function)

        ' Resume on the captured context, if one was available.
        Console.WriteLine(result)
    End Function

    Private Shared Async Function DownloadAsync() As Task(Of String)
        Await Task.Delay(100)
        Return "downloaded data"
    End Function

    Private Shared Function Compute(data As String) As String
        Return $"Computed: {data.Length} chars"
    End Function
End Class

通常,在控制台应用中,SynchronizationContext.Current通常是null,因此代码片段不会在实际的 UI 线程上恢复。 相反,这段代码从概念上说明了这条规则:如果 UI SynchronizationContextawait 个点流动,传递给 Task.Run 的委托中的 await 将把 UI 上下文视为 Current。 然后,await DownloadAsync() 之后的延续将发布回 UI 线程,导致 Compute(data) 在 UI 线程上运行,而不是在线程池上运行。 该行为会破坏Task.Run调用的目的。

因为运行时抑制了 ExecutionContext 中的 SynchronizationContext 流,所以 Task.Run 中的 await 不会继承外部 UI 上下文,并且延续会按预期在线程池上继续运行。

总结

方面 执行上下文 同步上下文
Purpose 跨异步边界传递环境状态 表示目标调度器(代码应运行的位置)
被捕获 异步方法生成器(基础结构) 任务 awaiter (await task)
跨等待流动? 是的,始终 否 - 已捕获并发布到,未流动
抑制 API ExecutionContext.SuppressFlow (高级;很少需要) ConfigureAwait(false)
范围 所有可等待对象 TaskTask<TResult> (自定义 awaiter 可以添加类似的逻辑)

另见