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. 概述
在前两篇文章中,我们探讨了 async
和 await
的编译过程和 Awaitable-Awaiter 模式。本文将深入探讨 await
在运行时的上下文处理机制,特别是如何通过 ExecutionContext
和 SynchronizationContext
解决跨线程问题。
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
的基础设施通过捕获和恢复 ExecutionContext
和 SynchronizationContext
来解决跨线程问题。
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 的 WindowsFormsSynchronizationContext
。await
会捕获初始线程的 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
为了在回调中正确处理 ExecutionContext
和 SynchronizationContext
,可以定义 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. 总结
- 编译时:
async
和await
被编译为状态机,代码按await
拆分为多个部分,并通过回调链式执行。 - 运行时:
ExecutionContext
总是被捕获,确保代码在正确的上下文中执行。SynchronizationContext
默认被捕获,确保 UI 操作在 UI 线程中执行,除非显式禁用(如ConfigureAwait(false)
)。
- 改进回调:通过
ContinueWithContext
可以正确处理ExecutionContext
和SynchronizationContext
,解决跨线程问题。