Understanding C# async / await 第3章

Table of Contents

Understanding C# async / await 第1章原文地址

Understanding C# async / await 第2章原文地址

Understanding C# async / await 第3章原文地址


以下是基于你提供的内容的总结和解读,保留示例代码和关键细节:


理解 C# 的 async / await (3):运行时上下文

1. 概述

在前两篇文章中,我们探讨了 asyncawait 的编译过程和 Awaitable-Awaiter 模式。本文将深入探讨 await 在运行时的上下文处理机制,特别是如何通过 ExecutionContextSynchronizationContext 解决跨线程问题。

2. 跨线程问题

在 WPF 应用程序中,以下代码可以正常工作:

this.Button.Click += async (sender, e) =>
{
    string html = await new WebClient().DownloadStringTaskAsync("https://weblogs.asp.net/dixin");
    this.TextBox.Text = html;
};

但如果将其改写为 Task.ContinueWith() 的回调形式:

this.Button.Click += (sender, e) =>
{
    new WebClient().DownloadStringTaskAsync("https://weblogs.asp.net/dixin").ContinueWith(await => 
    {
        string html = await.Result;
        this.TextBox.Text = html; // 可能会抛出 InvalidOperationException
    });
};

这段代码可能会抛出 InvalidOperationException,因为回调代码可能在一个非 UI 线程中执行,无法直接访问 UI 控件。

3. 运行时上下文处理

await 的基础设施通过捕获和恢复 ExecutionContextSynchronizationContext 来解决跨线程问题。

3.1 ExecutionContext

ExecutionContext 包含了当前线程的执行上下文信息,如安全上下文、调用上下文和同步上下文。await 会捕获初始线程的 ExecutionContext,并在每次调用 MoveNext() 时将其传递给下一个线程。

// 捕获当前线程的 ExecutionContext
ExecutionContext executionContext = ExecutionContext.Capture();

// 在指定的 ExecutionContext 中执行函数
public static TResult InvokeWith<TResult>(this Func<TResult> function, ExecutionContext executionContext)
{
    if (executionContext == null)
    {
        return function();
    }

    TResult result = default(TResult);
    ExecutionContext.Run(executionContext, _ => result = function(), null);
    return result;
}

3.2 SynchronizationContext

SynchronizationContext 提供了线程间的同步机制。在不同的环境中,SynchronizationContext 有不同的实现,如 WPF 的 DispatcherSynchronizationContext 和 WinForms 的 WindowsFormsSynchronizationContextawait 会捕获初始线程的 SynchronizationContext,并将 MoveNext() 的调用发布到该上下文中。

// 捕获当前线程的 SynchronizationContext
SynchronizationContext synchronizationContext = SynchronizationContext.Current;

// 在指定的 SynchronizationContext 和 ExecutionContext 中执行函数
public static Task<TResult> InvokeWith<TResult>(this Func<TResult> function, SynchronizationContext synchronizationContext, ExecutionContext executionContext)
{
    TaskCompletionSource<TResult> taskCompletionSource = new TaskCompletionSource<TResult>();
    try
    {
        if (synchronizationContext == null)
        {
            TResult result = function.InvokeWith(executionContext);
            taskCompletionSource.SetResult(result);
        }
        else
        {
            synchronizationContext.OperationStarted();
            synchronizationContext.Post(_ =>
            {
                try
                {
                    TResult result = function.InvokeWith(executionContext);
                    synchronizationContext.OperationCompleted();
                    taskCompletionSource.SetResult(result);
                }
                catch (Exception exception)
                {
                    taskCompletionSource.SetException(exception);
                }
            }, null);
        }
    }
    catch (Exception exception)
    {
        taskCompletionSource.SetException(exception);
    }
    return taskCompletionSource.Task;
}

4. 改进的 ContinueWithContext

为了在回调中正确处理 ExecutionContextSynchronizationContext,可以定义 ContinueWithContext 方法:

public static class TaskExtensions
{
    public static Task<TNewResult> ContinueWithContext<TResult, TNewResult>(this Task<TResult> task, Func<Task<TResult>, TNewResult> continuation)
    {
        ExecutionContext executionContext = ExecutionContext.Capture();
        SynchronizationContext synchronizationContext = SynchronizationContext.Current;
        return task.ContinueWith(t =>
                new Func<TNewResult>(() => continuation(t)).InvokeWith(synchronizationContext, executionContext))
            .Unwrap();
    }

    public static Task ContinueWithContext<TResult>(this Task<TResult> task, Action<Task<TResult>> continuation)
    {
        return task.ContinueWithContext(new Func<Task<TResult>, object>(t =>
        {
            continuation(t);
            return null;
        }));
    }
}

使用 ContinueWithContext 可以修复 WPF 应用程序中的跨线程问题:

this.Button.Click += (sender, e) =>
{
    new WebClient().DownloadStringTaskAsync("https://weblogs.asp.net/dixin").ContinueWithContext(await => 
    {
        string html = await.Result;
        this.TextBox.Text = html; // 现在可以正常工作
    });
};

5. 使用 Task.ConfigureAwait()

Task.ConfigureAwait() 提供了控制 SynchronizationContext 捕获的选项:

  • ConfigureAwait(true):默认行为,捕获 SynchronizationContext
  • ConfigureAwait(false):不捕获 SynchronizationContext

例如:

this.Button.Click += async (sender, e) =>
{
    await Task.Run(() => { }).ConfigureAwait(false);
    this.TextBox.Text = string.Empty; // 会抛出 InvalidOperationException
};

这段代码会抛出 InvalidOperationException,因为 ConfigureAwait(false) 阻止了 SynchronizationContext 的捕获。

6. 总结

  • 编译时asyncawait 被编译为状态机,代码按 await 拆分为多个部分,并通过回调链式执行。
  • 运行时
    • ExecutionContext 总是被捕获,确保代码在正确的上下文中执行。
    • SynchronizationContext 默认被捕获,确保 UI 操作在 UI 线程中执行,除非显式禁用(如 ConfigureAwait(false))。
  • 改进回调:通过 ContinueWithContext 可以正确处理 ExecutionContextSynchronizationContext,解决跨线程问题。
kumakoko avatar
kumakoko
pure coder
comments powered by Disqus