Skip to content

C#的异步设施

约 615 字大约 2 分钟

2026-06-21

同步上下文

在 WinForm 等 UI 中更 UI 控件需要回到 UI 线程执行。

这是一个使用同步上下文的例子:

var form = new Form();
var label = new Label { Text = "Initial text", Dock = DockStyle.Fill };
form.Controls.Add(label);

form.Shown += async (sender, e) =>
{
    // 拿到 UI 的同步上下文
    var syncContext = SynchronizationContext.Current;

    await Task.Run(() =>
    {
        // 在后台线程中运行
        Thread.Sleep(2000);
        Console.WriteLine($"Background thread: {Environment.CurrentManagedThreadId}");

        // 通过 syncContext 将操作调度回 UI 线程
        syncContext.Post(_ =>
        {
            // 如下代码会在 UI 线程中执行
            Console.WriteLine($"UI thread: {Environment.CurrentManagedThreadId}");
            label.Text = "Updated text";
        }, null);
    });
};

但使用 await 关键字时会自动捕获当前执行的上下文,所以这样即可:

form.Shown += async (sender, e) =>
{
    await Task.Delay(2000); // 非阻塞等待,自动回到 UI 线程
    Console.WriteLine($"UI thread: {Environment.CurrentManagedThreadId}");
    label.Text = "Updated text";
};

一个特殊例子:ConfigureAwait(false)

作用:不回到原先的同步上下文执行,默认情况是切换回到线程池执行。

说烂了这个例子:

using System.Windows.Forms;

// WinForm 的 UI 线程
public class DeadlockExample
{
    private static readonly HttpClient _http = new HttpClient();

    // 异步方法:内部没有 ConfigureAwait(false)
    private async Task<string> FetchDataAsync()
    {
        // await 会捕获 SynchronizationContext(UI 线程上下文)
        var response = await _http.GetStringAsync("https://httpbin.org/delay/1");
        return response.Substring(0, 10);  // 后续代码需要在原上下文执行
    }

    // UI 按钮点击事件(在 UI 线程执行)
    private void Button_Click(object sender, EventArgs e)
    {
        // ⚠️ 同步阻塞等待异步方法 → 死锁!
        string result = FetchDataAsync().Result;
        Console.WriteLine(result);  // 永远不会执行到这里
    }
}

在 UI 线程中使用 .Result / .Wait 同步阻塞方法,异步方法 FetchDataAsync 使用 await 时默认捕获了同步上下文,后续代码执行需要回到原来 同步上下文 执行也就是回到原来的 UI 线程执行。原来的 UI 线程被同步阻塞方法阻塞了,因此造成了死锁

解决方案

对于本例子可以改成:

await _http.GetStringAsync("https://httpbin.org/delay/1").ConfigureAwait(false)

或修改同步阻塞为 string result = await FetchDataAsync()。因为本例不更新 UI 所以可以不需要强制回到 UI 线程。

如果不关注上下文可以使用 ConfigureAwait(false)

现代场景

现在大多数情况都没有同步上下文了:

  • WinForm 你用吗?不用关注
  • WPF 你用吗?用的话可以关注
  • 控制台程序/后台线程?没有同步上下文
  • ASP.NET Core?没有同步上下文

只有那些自定义的环境需要同步上下文,这里定制需求就很高了。