第十四章:深度解密 async/await 与 Task 的底层原理
第十四章:深度解密 async/await 与 Task 的底层原理
- 第十四章:深度解密 async/await 与 Task 的底层原理
- 14.1 引言:从回调地狱到 async/await
- 14.2 Task 到底是什么?
- 14.3 async/await 的编译与运行
- 1. async/await 是什么?
- 2. async/await 的编译与执行流程
- 3.
AsyncTaskMethodBuilder<T>.AwaitUnsafeOnCompleted
源码解读- 完整源码
- 解读
- 整体概述
- 分步解析
- 1. 顶层方法:
AwaitUnsafeOnCompleted
- 2. 内部实现:
AwaitUnsafeOnCompleted
- 3. 包装状态机:
GetStateMachineBox
- 4. 注册回调:
AsyncTaskMethodBuilderT
的AwaitUnsafeOnCompleted
重载方法 - 5. 注册回调:
TaskAwaiter
的UnsafeOnCompletedInternal
- 6. 包装回调对象并将其添加到任务延续中:
Task.UnsafeSetContinuationForAwait
- 7. 注册回调到任务中:
AddTaskContinuation
- 8. 多回调处理:
AddTaskContinuationComplex
- 1. 顶层方法:
- 总结与重点
- 4. await 的核心
- 14.4 自定义Task、async/await实现
- 14.5 async/await 为什么这么好用?
- 14.6 async/await 与同步上下文的协作
- 14.7 async/await 与执行上下文的协作
- 14.8 AsyncLocal 与异步编程中的数据流转
- 14.9 async/await 与 Task 的常见误区
- 14.10 async/await 的性能优化与高级用法
14.1 引言:从回调地狱到 async/await
在现代软件开发中,异步编程是解决高并发、响应式交互和 I/O 密集型任务的重要方式。然而,早期的异步编程模型却充满了复杂性和陷阱。从最初的回调函数,到基于事件的异步模式(EAP),再到任务并行库(TPL)的引入,异步编程的历史是一段不断演进的旅程。最终,async/await
的出现被称为“异步编程的革命”,它以同步代码的形式实现了异步逻辑的表达,大幅提升了代码的可读性与可维护性。
1. 从回调函数到事件驱动:早期的异步编程模型
1.1 回调函数(Callback)的局限性
回调函数是最早用于异步编程的解决方案,其核心思想是:当一个异步操作完成后,调用一个预定义的函数来处理结果。例如,以下是一个典型的基于回调的异步操作:
void FetchData(string url, Action<string> callback)
{
// 模拟异步操作
ThreadPool.QueueUserWorkItem(_ =>
{
string data = $"Data from {url}";
callback(data);
});
}
// 使用回调
FetchData("https://example.com", result =>
{
Console.WriteLine($"Result: {result}");
});
虽然回调函数简单直观,但它有以下几个显著问题:
-
回调地狱(Callback Hell):
当多个异步操作需要按顺序执行时,回调函数会形成复杂的嵌套结构,导致代码难以阅读和维护。例如:fetchData((result1) => { // 拉取数据 ProcessDataAsync(result1, (result2) => { // 处理数据 saveData(result2, (result3) => { // 保存数据 console.log("All operations complete"); }); }); });
这种代码结构不仅让程序员难以理解,还容易引入错误。
-
错误处理困难:
异常必须通过回调显式传递,而不能使用传统的try/catch
进行捕获。例如:fetchData((result, error) => { if (error) { console.error("Error:", error); } else { console.log(result); } });
-
可读性和可维护性差:
回调函数让代码逻辑变得琐碎且冗长,增加了调试和测试的复杂性。
1.2 基于事件的异步模式(EAP)
为了改善回调函数的局限性,.NET 提出了基于事件的异步模式(Event-based Asynchronous Pattern, EAP)。EAP 的核心思想是:通过事件通知异步操作的完成,并允许开发者注册事件处理程序。例如:
FileDownloader downloader = new FileDownloader();
downloader.DownloadCompleted += (sender, e) =>
{
if (e.Error == null)
{
Console.WriteLine("Download completed: " + e.Result);
}
else
{
Console.WriteLine("Error: " + e.Error.Message);
}
};
downloader.DownloadAsync("http://example.com/file");
虽然 EAP 改善了回调函数的嵌套问题,但它仍然存在显著的缺点:
-
事件管理复杂:
需要手动管理事件订阅和取消,可能导致内存泄漏(如事件未被正确解除订阅)。 -
错误处理分散:
异常仍然需要通过事件参数传递,不能直接使用try/catch
。 -
代码结构分散:
逻辑代码分布在事件处理程序中,仍然难以维护。
1.3 任务并行库(TPL)和 Task
的引入
为了解决回调和 EAP 的问题,.NET 在 .NET Framework 4 中引入了 任务并行库(Task Parallel Library, TPL),核心是 Task
类型。Task
将异步操作的结果封装为对象,并提供了更强大的功能,如链式调用和统一的异常处理机制。
以下是使用 Task
的示例:
Task.Run(() =>
{
return DownloadFile("http://example.com/file");
}).ContinueWith(task =>
{ // 处理后续任务
if (task.IsFaulted)
{
Console.WriteLine("Error: " + task.Exception.InnerException.Message);
}
else
{
Console.WriteLine("Download completed: " + task.Result);
}
});
优点:
-
支持链式调用:
使用ContinueWith
可以将多个异步操作串联起来,避免了嵌套的回调地狱。 -
统一的异常处理:
异常被封装在Task.Exception
中,支持统一的处理方式。 -
更强的灵活性:
支持并发任务的管理(如Task.WhenAll
和Task.WhenAny
)。
局限性:
- 虽然
Task
改善了代码的结构,但依然存在一定的复杂性,尤其是在处理多个嵌套任务时。
2. async/await:优雅地解决回调地狱
2.1 async/await 的设计哲学
async/await
是基于 Task
的进一步封装,旨在解决异步编程中的可读性和维护性问题。它允许开发者以同步的编码风格编写异步代码,同时避免了回调地狱和复杂的任务链式调用。
以下是一个使用 async/await
的示例:
public async Task DownloadFilesAsync()
{
try
{
string result = await DownloadFileAsync("http://example.com/file");
Console.WriteLine("Download completed: " + result);
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.Message);
}
}
特点:
-
同步风格的代码:
代码从上到下顺序执行,逻辑清晰,避免了嵌套和分散。 -
内置异常处理:
异常可以通过try/catch
捕获,不需要额外的事件处理或错误回调。 -
与
Task
无缝集成:
async/await
是对Task
的扩展,完全兼容已有的任务并行库。
2.2 async/await 如何优雅地解决回调地狱?
-
摒弃嵌套:
使用await
可以直接等待异步操作的完成,而不需要嵌套回调。例如:// 回调地狱示例: FetchData((data) => { ProcessDataAsync(data, (processed) => { SaveData(processed, (saved) => { Console.WriteLine("All done!"); }); }); }); // 使用 async/await: async Task DoWorkAsync() { var data = await FetchDataAsync(); var processed = await ProcessDataAsyncAsync(data); await SaveDataAsync(processed); Console.WriteLine("All done!"); }
-
逻辑清晰:
通过await
,异步代码的执行顺序变得更加直观,更接近同步代码的风格。 -
错误处理简单:
不需要手动订阅错误回调或检查异常状态,try/catch
即可处理所有异常。
3. async/await:异步编程的革命
3.1 更易读、更易维护
-
同步化的异步代码:
使用async/await
,开发者可以以同步的方式组织异步逻辑,大幅提升代码的可读性和可维护性。 -
错误处理统一:
异步方法的异常可以通过try/catch
捕获,与同步方法无异。 -
消除嵌套:
不再需要嵌套的回调函数或复杂的任务链式调用。
3.2 更高效的资源利用
-
非阻塞模型:
await
的本质是挂起当前方法,释放线程资源,等待异步操作完成后继续执行。 -
线程池的高效利用:
async/await 避免了传统模型中线程的空闲等待,提升了资源利用率。
总结
从回调函数到 EAP,再到 TPL 和 async/await,异步编程经历了从复杂到优雅的演变过程。async/await
被称为“异步编程的革命”,因为它让开发者以同步代码的风格编写异步逻辑,大幅提升了代码的可读性和维护性,同时充分利用了系统资源。它的出现,标志着异步编程进入了一个全新的时代。
14.2 Task 到底是什么?
在 .NET 的异步编程模型中,Task
是一个核心概念。它是任务并行库(Task Parallel Library, TPL)的基础,也是 async/await
语法的基石。尽管 Task
经常被用来处理异步操作,但它并不是一个简单的线程,而是一个更高级的抽象。为了深入理解 Task
,我们需要剖析它的角色、核心组件以及不同的类型。
1. Task 的角色
1.1 Task 是线程吗?为什么它不是线程?
很多人初学时会误认为Task
直接与线程关联,但实际上,Task
是一个 异步操作的抽象
,并不直接映射到任何具体的线程。。以下是 Task
和线程的本质区别:
-
线程的本质:
- 线程是操作系统分配 CPU 时间的基本单位,每个线程都有自己的堆栈和上下文。
- 线程的生命周期由操作系统管理,线程的创建和销毁开销较大。
-
Task 的本质:
Task
是一个逻辑任务的抽象,可以代表任意的异步工作(如 I/O 操作、计算任务)。Task
并不直接创建线程,而是可能在某个线程上运行,大多数都在线程池线程上执行,也可能不依赖线程(例如等待 I/O 操作时)。Task
的调度由TaskScheduler
管理,线程池中的线程会被复用以减少开销。
总结:
Task
是一个轻量化的、面向逻辑的异步操作的容器,它的实现与线程解耦,但可以利用线程池中的线程来执行任务。通过这种设计,Task
提供了更高效的资源管理和灵活性。
1.2 Task 是如何管理异步操作状态的?
Task
的核心功能是管理异步操作的状态和结果。内部有一个状态机,能够跟踪任务的生命周期,并提供相关的状态信息。这些状态包括:
- Pending(等待中): 任务尚未开始执行。
- Running(运行中): 任务正在执行。
- Completed(已完成): 任务成功完成。
- Faulted(出错): 任务执行过程中发生了未处理的异常。
- Canceled(已取消): 任务被取消。
Task
提供了以下机制来管理状态:
-
状态查询:
可以通过Task.Status
属性查看任务的当前状态。 -
结果管理:
通过Task.Result
获取任务的返回值(如果任务没有完成,会阻塞调用线程)。 -
错误处理:
通过Task.Exception
获取任务中未捕获的异常。 -
取消支持:
通过CancellationToken
支持任务的取消操作。
Task
通过内部状态机实现这些状态管理,并暴露了 IsCompleted
、IsFaulted
等属性方便开发者检查状态。
Task
的这种状态管理机制,使得异步操作的执行过程变得透明且易于跟踪。
2. Task 的核心组件
2.1 Task 的生命周期
一个 Task
的生命周期可以分为以下几个阶段:
-
创建:
使用Task
或Task.Run
创建一个任务,但此时任务尚未开始。Task task = new Task(() => Console.WriteLine("Hello, Task!"));
-
调度:
调用Task.Start()
或直接使用Task.Run()
会将任务提交到任务调度器中。task.Start(); // 或者直接使用 Task.Run()
-
运行:
任务开始执行,进入Running
状态。 -
完成:
如果任务成功执行完毕,进入Completed
状态。 -
取消或失败:
如果任务被取消或发生未处理的异常,分别进入Canceled
或Faulted
状态。try { await task; } catch (Exception ex) { Console.WriteLine($"Task failed: {ex.Message}"); }
2.2 TaskScheduler 与线程池的关系
Task
的执行依赖于 TaskScheduler
,它负责将任务分配到合适的线程中。默认情况下,TaskScheduler
使用线程池(ThreadPool)来调度任务。
更详细内容参考第十三章:调度
-
线程池的作用:
- 线程池是一个线程复用机制,可以避免频繁创建和销毁线程的开销。
- 线程池中的线程是动态分配的,能够根据系统负载调整线程数量。
-
TaskScheduler 的作用:
TaskScheduler
是一个抽象类,定义了任务的调度逻辑。- 默认实现是
ThreadPoolTaskScheduler
,它将任务调度到线程池中执行。
开发者也可以自定义 TaskScheduler
,用于特定场景(如限制任务并发数量)。
TaskCompletionSource 的作用
在底层,Task
是通过状态机实现的。状态机会跟踪任务的状态,并在任务完成时触发相关的回调逻辑。
更多内容参考第八章:封装与互操作
-
TaskCompletionSource:
TaskCompletionSource
是一个用于手动控制Task
状态的工具,开发者可以通过它来完成、取消或设置任务失败。它常用于将非基于Task
的异步操作包装为Task
。示例:
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>(); Task<int> task = tcs.Task; // 在某个异步操作完成后设置结果 tcs.SetResult(42); Console.WriteLine(await task); // 输出 42
通过 TaskCompletionSource
,开发者可以更灵活地控制任务的状态转换。
3. Task 的类型
3.1 普通的 Task
和返回值的 Task<T>
Task
有两种基本类型:
-
Task
:
不返回结果的任务,用于执行不需要返回值的异步操作。Task task = Task.Run(() => Console.WriteLine("Hello, Task!"));
-
Task<T>
:
返回结果的任务,用于执行需要返回值的异步操作。Task<int> task = Task.Run(() => 42); int result = await task; Console.WriteLine(result); // 输出 42
Task<T>
提供了类型安全的方式来获取异步操作的结果。
3.2 ValueTask 的引入及其适用场景
为了优化 Task
在高频调用场景下的性能问题,.NET 引入了 ValueTask
,减少任务分配的开销。
-
ValueTask
的特点:- 它可以避免频繁分配堆内存(
Task
通常会在堆上分配)。 - 如果任务已经完成,可以直接返回结果,而无需生成新的任务对象。
- 它可以避免频繁分配堆内存(
-
适用场景:
- 高频调用的异步方法。
- 大多数情况下任务已经完成,但仍需要支持异步操作。
示例:
async ValueTask<int> ComputeAsync(bool quick) { if (quick) { return 42; // 同步完成,避免分配额外的 Task 对象 } // 模拟异步完成 return await Task.Delay(1000).ContinueWith(_ => 42); } // 使用 int result = await ComputeAsync(true);
-
注意事项:
ValueTask
不能多次await
或重复使用。- 不适用于所有场景,在复杂任务链中仍建议使用
Task
。
14.3 async/await 的编译与运行
1. async/await 是什么?
1.1 async/await 是语法糖
async/await
是一种语法糖,它的作用是让开发者以同步的方式编写异步代码。然而,在运行时,async/await
会被编译器拆解为一个状态机,通过状态机管理异步操作的执行流程。
示例代码:
public async Task<int> FetchDataAsync()
{
int result = await GetDataAsync();
return result * 2;
}
这段代码看似同步,但它在编译后会被重写为状态机,异步操作的各个步骤都会被拆解为状态机的不同状态,并在状态之间流转。
1.2 async/await 的核心:状态机
async
方法的核心是 编译器生成的状态机,它将异步方法拆解为多个状态,并根据异步操作的完成情况在这些状态之间切换。
状态机的职责:
-
保存上下文:
异步方法的局部变量和当前状态会被保存在状态机中,以便在异步操作完成后恢复执行。 -
管理状态流转:
异步操作完成后,状态机会根据当前状态执行相应的逻辑,直到方法执行完毕。 -
挂起和恢复:
当遇到await
时,状态机会挂起当前方法,并在异步任务完成时恢复执行。
2. async/await 的编译与执行流程
编译器如何将 async 方法拆解为状态机?
2.1 异步示例代码
// 下载器
public class Downloader
{
// 异步方法拉取数据
static async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url); // 网卡 I/O 异步获取数据
}
// 处理数据,虽然返回Task<T>,但是是同步方法,只不过方法启动并返回了一个Task对象,并不执行任何异步操作(await ...)
static Task<string> ProcessDataAsyncAsync(string html)
{
return Task.Run(() =>
{
Thread.Sleep(1000);// 阻塞线程线程池线程1s,模拟 CPU 密集耗时操作
return html.ToUpper(); // 示例处理逻辑
});
}
// 异步保存数据
static async Task SaveDataAsync(string html)
{
await File.WriteAllTextAsync("index.html", html); // 磁盘 I/O 异步保存
}
// 异步从url拉取数据、处理、保存数据
static public async Task SaveDataFromUrlAsync()
{
string url = "https://www.baidu.com";
string html = await FetchDataAsync(url);// 拉取数据
string processedHtml = await ProcessDataAsyncAsync(html);// 处理数据
await SaveDataAsync(processedHtml);// 保存数据
}
}
2.2 ILSpy反编译源码
通过ILSpy
将编译后生成的源码文件反编译后:
ILSpy反编译:
得到如图所示:
完整代码:
using ...
[NullableContext(1)]
[Nullable(0)]
public class Downloader
{
[CompilerGenerated]
private sealed class <FetchDataAsync>d__0 : IAsyncStateMachine
{
public int <>1__state;
[Nullable(0)]
public AsyncTaskMethodBuilder<string> <>t__builder;
[Nullable(0)]
public string url;
[Nullable(0)]
private HttpClient <client>5__1;
[Nullable(0)]
private string <>s__2;
[Nullable(new byte[] { 0, 1 })]
private TaskAwaiter<string> <>u__1;
private void MoveNext()
{
int num = <>1__state;
string result;
try
{
if (num != 0)
{
<client>5__1 = new HttpClient();
}
try
{
TaskAwaiter<string> awaiter;
if (num != 0)
{
awaiter = <client>5__1.GetStringAsync(url).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<FetchDataAsync>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
}
<>s__2 = awaiter.GetResult();
result = <>s__2;
}
finally
{
if (num < 0 && <client>5__1 != null)
{
((IDisposable)<client>5__1).Dispose();
}
}
}
catch (Exception exception)
{
<>1__state = -2;
<client>5__1 = null;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<client>5__1 = null;
<>t__builder.SetResult(result);
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[CompilerGenerated]
private sealed class <SaveDataAsync>d__2 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
[Nullable(0)]
public string html;
private TaskAwaiter <>u__1;
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = File.WriteAllTextAsync("index.html", html).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<SaveDataAsync>d__2 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[CompilerGenerated]
private sealed class <SaveDataFromUrlAsync>d__3 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
[Nullable(0)]
private string <url>5__1;
[Nullable(0)]
private string <html>5__2;
[Nullable(0)]
private string <processedHtml>5__3;
[Nullable(0)]
private string <>s__4;
[Nullable(0)]
private string <>s__5;
[Nullable(new byte[] { 0, 1 })]
private TaskAwaiter<string> <>u__1;
private TaskAwaiter <>u__2;
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter<string> awaiter3;
TaskAwaiter<string> awaiter2;
TaskAwaiter awaiter;
switch (num)
{
default:
<url>5__1 = "https://www.baidu.com";
awaiter3 = FetchDataAsync(<url>5__1).GetAwaiter();
if (!awaiter3.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter3;
<SaveDataFromUrlAsync>d__3 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine);
return;
}
goto IL_0090;
case 0:
awaiter3 = <>u__1;
<>u__1 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
goto IL_0090;
case 1:
awaiter2 = <>u__1;
<>u__1 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
goto IL_010d;
case 2:
{
awaiter = <>u__2;
<>u__2 = default(TaskAwaiter);
num = (<>1__state = -1);
break;
}
IL_010d:
<>s__5 = awaiter2.GetResult();
<processedHtml>5__3 = <>s__5;
<>s__5 = null;
awaiter = SaveDataAsync(<processedHtml>5__3).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 2);
<>u__2 = awaiter;
<SaveDataFromUrlAsync>d__3 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
break;
IL_0090:
<>s__4 = awaiter3.GetResult();
<html>5__2 = <>s__4;
<>s__4 = null;
awaiter2 = ProcessDataAsync(<html>5__2).GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (<>1__state = 1);
<>u__1 = awaiter2;
<SaveDataFromUrlAsync>d__3 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
return;
}
goto IL_010d;
}
awaiter.GetResult();
}
catch (Exception exception)
{
<>1__state = -2;
<url>5__1 = null;
<html>5__2 = null;
<processedHtml>5__3 = null;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<url>5__1 = null;
<html>5__2 = null;
<processedHtml>5__3 = null;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[AsyncStateMachine(typeof(<FetchDataAsync>d__0))]
[DebuggerStepThrough]
private static Task<string> FetchDataAsync(string url)
{
<FetchDataAsync>d__0 stateMachine = new <FetchDataAsync>d__0();
stateMachine.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
stateMachine.url = url;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
private static Task<string> ProcessDataAsync(string html)
{
return Task.Run([NullableContext(0)] () =>
{
Thread.Sleep(1000);
return html.ToUpper();
});
}
[AsyncStateMachine(typeof(<SaveDataAsync>d__2))]
[DebuggerStepThrough]
private static Task SaveDataAsync(string html)
{
<SaveDataAsync>d__2 stateMachine = new <SaveDataAsync>d__2();
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.html = html;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
[AsyncStateMachine(typeof(<SaveDataFromUrlAsync>d__3))]
[DebuggerStepThrough]
public static Task SaveDataFromUrlAsync()
{
<SaveDataFromUrlAsync>d__3 stateMachine = new <SaveDataFromUrlAsync>d__3();
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
}
2.3 编译器生成的异步状态机
完整可执行的状态机
将2.2去除掉多余的Attribute和一些特殊符号“<”、“>”,美化一下,然后再加上注释,就成了如下可以直接运行的代码:
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace SimpleTest;
public class Downloader
{
private sealed class FetchDataAsyncStateMachine : IAsyncStateMachine
{
// 当前状态(-1: 初始状态,0: 挂起状态,-2: 完成状态)
public int state;
// 异步任务的构建器,用于管理任务的生命周期
public AsyncTaskMethodBuilder<string> taskBuilder;
// 输入参数:目标 URL
public string url;
// 内部变量:用于 HTTP 请求的 HttpClient
private HttpClient client;
private string fetchedData; // 保存从 URL 获取的数据
private TaskAwaiter<string> awaiter; // 用于管理 GetStringAsync 的等待状态
public void MoveNext()
{
int currentState = state; // 保存当前状态
string result;
try
{
if (currentState != 0) // 状态为初始状态
{
client = new HttpClient(); // 创建 HttpClient 实例
}
try
{
TaskAwaiter<string> taskAwaiter;
if (currentState != 0) // 状态为初始状态
{
// 开始异步操作,获取 URL 的内容
taskAwaiter = client.GetStringAsync(url).GetAwaiter();
// 如果异步操作未完成,挂起当前状态机
if (!taskAwaiter.IsCompleted)
{
state = 0; // 设置状态为挂起状态
awaiter = taskAwaiter; // 保存当前的 TaskAwaiter
FetchDataAsyncStateMachine stateMachine = this;
// 将状态机挂起,等待异步操作完成后继续
taskBuilder.AwaitUnsafeOnCompleted(ref taskAwaiter, ref stateMachine);
return; // 返回以挂起当前逻辑
}
}
else // 从挂起状态恢复执行
{
taskAwaiter = awaiter; // 恢复挂起时保存的 TaskAwaiter
awaiter = default; // 清空挂起状态
state = -1; // 设置状态为已恢复
}
// 获取异步操作的结果
fetchedData = taskAwaiter.GetResult();
result = fetchedData;
}
finally
{
// 在操作完成后释放 HttpClient 资源
if (state < 0 && client != null)
{
client.Dispose();
}
}
}
catch (Exception exception)
{
// 异常处理:设置状态为完成并报告异常
state = -2;
client = null;
taskBuilder.SetException(exception);
return;
}
// 设置状态为完成并返回结果
state = -2;
client = null;
taskBuilder.SetResult(result);
}
// 必须实现的接口方法,当前示例中未使用
public void SetStateMachine(IAsyncStateMachine stateMachine) { }
}
private sealed class SaveDataAsyncStateMachine : IAsyncStateMachine
{
// 当前状态(-1: 初始状态,0: 挂起状态,-2: 完成状态)
public int state;
// 异步任务的构建器,用于管理任务的生命周期
public AsyncTaskMethodBuilder taskBuilder;
// 输入参数:要写入文件的 HTML 数据
public string html;
// 内部变量:管理 WriteAllTextAsync 的等待状态
private TaskAwaiter awaiter;
public void MoveNext()
{
int currentState = state; // 保存当前状态
try
{
TaskAwaiter taskAwaiter;
if (currentState != 0) // 初始状态
{
// 开始异步写入操作
taskAwaiter = File.WriteAllTextAsync("index.html", html).GetAwaiter();
// 如果写入操作未完成,挂起当前状态机
if (!taskAwaiter.IsCompleted)
{
state = 0; // 设置状态为挂起状态
awaiter = taskAwaiter; // 保存当前的 TaskAwaiter
SaveDataAsyncStateMachine stateMachine = this;
// 将状态机挂起,等待写入操作完成后继续
taskBuilder.AwaitUnsafeOnCompleted(ref taskAwaiter, ref stateMachine);
return; // 返回以挂起当前逻辑
}
}
else // 从挂起状态恢复执行
{
taskAwaiter = awaiter; // 恢复挂起时保存的 TaskAwaiter
awaiter = default; // 清空挂起状态
state = -1; // 设置状态为已恢复
}
// 获取异步操作的结果(此处无返回值,单纯确保无异常)
taskAwaiter.GetResult();
}
catch (Exception exception)
{
// 异常处理:设置状态为完成并报告异常
state = -2;
taskBuilder.SetException(exception);
return;
}
// 设置状态为完成
state = -2;
taskBuilder.SetResult();
}
// 必须实现的接口方法,当前示例中未使用
public void SetStateMachine(IAsyncStateMachine stateMachine) { }
}
private sealed class SaveDataFromUrlAsyncStateMachine : IAsyncStateMachine
{
// 当前状态(-1: 初始状态,0,1,2: 挂起状态,-2: 完成状态)
public int state;
public AsyncTaskMethodBuilder taskBuilder;
// 内部变量
private string url; // 请求的 URL
private string html; // 获取的 HTML 数据
private string processedHtml; // 处理后的 HTML 数据
// 临时变量
private string tempHtmlResult;
private string tempProcessedResult;
// 用于管理多个异步操作的 Awaiter
private TaskAwaiter<string> awaiter3; // 对应 FetchDataAsync
private TaskAwaiter<string> awaiter2; // 对应 ProcessDataAsync
private TaskAwaiter awaiter; // 对应 SaveDataAsync
public void MoveNext()
{
int currentState = state; // 保存当前状态机的状态。初始值为 -1,表示尚未开始执行。
try
{
TaskAwaiter<string> stringTaskAwaiter; // 用于管理异步操作 `FetchDataAsync` 和 `ProcessDataAsync` 的结果。
TaskAwaiter simpleAwaiter; // 用于管理异步操作 `SaveDataAsync` 的结果。
// 根据当前状态执行不同的逻辑。
switch (currentState)
{
default: // 初始状态(state = -1)
url = "https://www.baidu.com"; // 初始化 URL 变量,表示目标地址。
// 调用 FetchDataAsync 方法以获取 HTML 数据,并获取其 Awaiter。
stringTaskAwaiter = FetchDataAsync(url).GetAwaiter();
if (!stringTaskAwaiter.IsCompleted) // 如果异步操作尚未完成,则需要挂起状态机。
{
state = 0; // 将状态设置为 0,表示挂起点在 FetchDataAsync 处。
awaiter3 = stringTaskAwaiter; // 保存当前的 Awaiter(对应 FetchDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存当前状态机实例。
// 挂起状态机,并在异步操作完成后恢复执行。
taskBuilder.AwaitUnsafeOnCompleted(ref stringTaskAwaiter, ref stateMachine);
return; // 退出方法,等待异步操作完成时重新进入。
}
goto Case_FetchCompleted; // 如果异步操作已完成,直接跳转到 FetchDataAsync 完成后的逻辑。
case 0: // 从 FetchDataAsync 挂起点恢复
stringTaskAwaiter = awaiter3; // 恢复之前保存的 Awaiter。
awaiter3 = default; // 清除 Awaiter 的引用。
state = -1; // 重置状态为 -1,表示状态机当前未挂起。
goto Case_FetchCompleted; // 跳转到 FetchDataAsync 完成后的逻辑。
case 1: // 从 ProcessDataAsync 挂起点恢复
stringTaskAwaiter = awaiter2; // 恢复之前保存的 Awaiter。
awaiter2 = default; // 清除 Awaiter 的引用。
state = -1; // 重置状态为 -1,表示状态机当前未挂起。
goto Case_ProcessCompleted; // 跳转到 ProcessDataAsync 完成后的逻辑。
case 2: // 从 SaveDataAsync 挂起点恢复
simpleAwaiter = awaiter; // 恢复之前保存的 Awaiter。
awaiter = default; // 清除 Awaiter 的引用。
state = -1; // 重置状态为 -1,表示状态机当前未挂起。
break;
Case_FetchCompleted: // FetchDataAsync 操作完成,处理结果
tempHtmlResult = stringTaskAwaiter.GetResult(); // 获取 FetchDataAsync 的返回结果(HTML 数据)。
html = tempHtmlResult; // 将结果赋值给 html 变量。
tempHtmlResult = null; // 清理临时变量。
// 调用 ProcessDataAsync 方法以处理 HTML 数据,并获取其 Awaiter。
stringTaskAwaiter = ProcessDataAsync(html).GetAwaiter();
if (!stringTaskAwaiter.IsCompleted) // 如果异步操作尚未完成,则需要挂起状态机。
{
state = 1; // 将状态设置为 1,表示挂起点在 ProcessDataAsync 处。
awaiter2 = stringTaskAwaiter; // 保存当前的 Awaiter(对应 ProcessDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存当前状态机实例。
// 挂起状态机,并在异步操作完成后恢复执行。
taskBuilder.AwaitUnsafeOnCompleted(ref stringTaskAwaiter, ref stateMachine);
return; // 退出方法,等待异步操作完成时重新进入。
}
goto Case_ProcessCompleted; // 如果异步操作已完成,直接跳转到 ProcessDataAsync 完成后的逻辑。
Case_ProcessCompleted: // ProcessDataAsync 操作完成,处理结果
tempProcessedResult = stringTaskAwaiter.GetResult(); // 获取 ProcessDataAsync 的返回结果(处理后的 HTML 数据)。
processedHtml = tempProcessedResult; // 将结果赋值给 processedHtml 变量。
tempProcessedResult = null; // 清理临时变量。
// 调用 SaveDataAsync 方法以保存处理后的数据,并获取其 Awaiter。
simpleAwaiter = SaveDataAsync(processedHtml).GetAwaiter();
if (!simpleAwaiter.IsCompleted) // 如果异步操作尚未完成,则需要挂起状态机。
{
state = 2; // 将状态设置为 2,表示挂起点在 SaveDataAsync 处。
awaiter = simpleAwaiter; // 保存当前的 Awaiter(对应 SaveDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存当前状态机实例。
// 挂起状态机,并在异步操作完成后恢复执行。
taskBuilder.AwaitUnsafeOnCompleted(ref simpleAwaiter, ref stateMachine);
return; // 退出方法,等待异步操作完成时重新进入。
}
break; // 如果异步操作已完成,直接执行 SaveDataAsync 完成后的逻辑。
}
// SaveDataAsync 操作完成,确保任务成功结束
simpleAwaiter.GetResult(); // 调用 GetResult 确保 SaveDataAsync 没有抛出异常。
}
catch (Exception exception) // 捕获异步操作中可能抛出的任何异常
{
state = -2; // 将状态机的状态设置为 -2,表示已完成且发生异常。
taskBuilder.SetException(exception); // 将捕获的异常传递给 TaskBuilder,通知调用方任务失败。
return; // 退出方法,状态机终止。
}
// 异步任务成功完成
state = -2; // 将状态机的状态设置为 -2,表示已完成且没有异常。
taskBuilder.SetResult(); // 标记任务完成并通知调用方。
}
public void SetStateMachine(IAsyncStateMachine stateMachine) { }
}
private static Task<string> FetchDataAsync(string url) // 异步方法,接收一个 URL 参数,返回一个包含字符串结果的任务。
{
var stateMachine = new FetchDataAsyncStateMachine // 创建 FetchDataAsyncStateMachine 状态机实例。
{
taskBuilder = AsyncTaskMethodBuilder<string>.Create(), // 初始化 TaskBuilder,用于构建异步任务。
url = url, // 将调用方传入的 URL 参数赋值到状态机中。
state = -1 // 初始化状态为 -1,表示状态机尚未开始执行。
};
stateMachine.taskBuilder.Start(ref stateMachine); // 启动状态机,开始执行其 MoveNext 方法。
return stateMachine.taskBuilder.Task; // 返回由 TaskBuilder 创建的任务,供调用方等待异步操作完成。
}
private static Task<string> ProcessDataAsync(string html) // 异步方法,接收 HTML 字符串,返回处理后的字符串任务。
{
return Task.Run(() => // 使用 Task.Run 在线程池中执行同步代码,模拟异步操作。
{
Thread.Sleep(1000); // 模拟耗时操作,例如数据处理或计算。
return html.ToUpper(); // 将输入 HTML 转换为大写后返回。
});
}
private static Task SaveDataAsync(string html) // 异步方法,接收 HTML 字符串,返回一个任务。
{
var stateMachine = new SaveDataAsyncStateMachine // 创建 SaveDataAsyncStateMachine 状态机实例。
{
taskBuilder = AsyncTaskMethodBuilder.Create(), // 初始化 TaskBuilder,用于构建不返回值的异步任务。
html = html, // 将调用方传入的 HTML 参数赋值到状态机中。
state = -1 // 初始化状态为 -1,表示状态机尚未开始执行。
};
stateMachine.taskBuilder.Start(ref stateMachine); // 启动状态机,开始执行其 MoveNext 方法。
return stateMachine.taskBuilder.Task; // 返回由 TaskBuilder 创建的任务,供调用方等待异步操作完成。
}
public static Task SaveDataFromUrlAsync() // 异步方法,无参数,返回一个任务。
{
var stateMachine = new SaveDataFromUrlAsyncStateMachine // 创建 SaveDataFromUrlAsyncStateMachine 状态机实例。
{
taskBuilder = AsyncTaskMethodBuilder.Create(), // 初始化 TaskBuilder,用于构建不返回值的异步任务。
state = -1 // 初始化状态为 -1,表示状态机尚未开始执行。
};
stateMachine.taskBuilder.Start(ref stateMachine); // 启动状态机,开始执行其 MoveNext 方法。
return stateMachine.taskBuilder.Task; // 返回由 TaskBuilder 创建的任务,供调用方等待异步操作完成。
}
}
注意:示例使用的是
Debug
发布模式,生成的状态机是一个Class,如果使用的是Release
模式发布,生成的状态机会是一个结构体,以优化性能。
状态机执行步骤
通过对比异步代码和编译器生成的状态机的代码,编译器为每个异步方法生成一个独立的状态机类型,这些类实现了 IAsyncStateMachine 接口。
调用并执行一个异步方法实际上就是启动一个该异步方法对应的状态机实例。
第一步:启动状态机
private static Task<string> FetchDataAsync(string url) // 异步方法,接收一个 URL 参数,返回一个包含字符串结果的任务。
{
var stateMachine = new FetchDataAsyncStateMachine // 创建 FetchDataAsyncStateMachine 状态机实例。
{
taskBuilder = AsyncTaskMethodBuilder<string>.Create(), // 初始化 TaskBuilder,用于构建异步任务。
url = url, // 将调用方传入的 URL 参数赋值到状态机中。
state = -1 // 初始化状态为 -1,表示状态机尚未开始执行。
};
stateMachine.taskBuilder.Start(ref stateMachine); // 启动状态机,开始执行其 MoveNext 方法。
return stateMachine.taskBuilder.Task; // 返回由 TaskBuilder 创建的任务,供调用方等待异步操作完成。
}
其中taskBuilder.Start(ref stateMachine)
源码如下:
/// <summary>
/// 启动状态机的执行。
/// </summary>
/// <typeparam name="TStateMachine">状态机的类型。</typeparam>
/// <param name="stateMachine">状态机实例,按引用传递。</param>
[DebuggerStepThrough]
public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
if (stateMachine == null) // 确保状态机实例不为 null
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
}
// 获取当前线程的执行上下文和同步上下文
Thread currentThread = Thread.CurrentThread;
ExecutionContext? previousExecutionCtx = currentThread._executionContext;
SynchronizationContext? previousSyncCtx = currentThread._synchronizationContext;
try
{
stateMachine.MoveNext(); // 执行状态机的下一步逻辑
}
finally
{
// 如果同步上下文发生了变化,恢复为之前的同步上下文
if (previousSyncCtx != currentThread._synchronizationContext)
{
currentThread._synchronizationContext = previousSyncCtx;
}
// 如果执行上下文发生了变化,恢复为之前的执行上下文
if (previousExecutionCtx != currentThread._executionContext)
{
ExecutionContext.RestoreChangedContextToThread(currentThread, previousExecutionCtx, currentThread._executionContext);
}
}
}
其实就只是开始执行状态机的MoveNext
方法,同时捕获并在执行后恢复线程的同步上下文和执行上下文,确保上下文一致性不被意外修改。
详细观察状态机的 MoveNext
方法可以发现,异步方法被编译器通过 await 拆分为多个逻辑块,每个块通常对应一个 await 操作(异步操作),比如异步方法 SaveDataFromUrlAsync
的状态机的MoveNext
方法中:
第二步:启动异步方法FetchDataAsync
当状态机的状态是-1
时候,也就是初始状态,MoveNext执行如下内容:
// 根据当前状态执行不同的逻辑。
switch (currentState)
{
default: // 初始状态(state = -1)
url = "https://www.baidu.com"; // 初始化 URL 变量,表示目标地址。
// 调用 FetchDataAsync 方法以获取 HTML 数据,并获取其 Awaiter。
stringTaskAwaiter = FetchDataAsync(url).GetAwaiter();
if (!stringTaskAwaiter.IsCompleted) // 如果异步操作尚未完成,则需要挂起状态机。
{
state = 0; // 将状态设置为 0,表示挂起点在 FetchDataAsync 处。
awaiter3 = stringTaskAwaiter; // 保存当前的 Awaiter(对应 FetchDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存当前状态机实例。
// 挂起状态机,并在异步操作`FetchDataAsync`完成后恢复执行。
taskBuilder.AwaitUnsafeOnCompleted(ref stringTaskAwaiter, ref stateMachine);
return; // 退出方法,等待异步`FetchDataAsync`操作完成时重新进入。
}
...
}
...
重点解读:
- 启动异步方法
FetchDataAsync
- 将状态机的状态从-1设置成0。
- 然后执行
taskBuilder.AwaitUnsafeOnCompleted
- 然后就返回了?嗯?
MoveNext
就执行完返回了吗,那后续状态怎么执行,在哪里执行。往后看!
第三步:启动异步方法ProcessDataAsync
当状态机的状态是0
时候,MoveNext
执行如下内容:
case 0: // 从 FetchDataAsync 挂起点恢复
stringTaskAwaiter = awaiter3; // 恢复之前保存的 Awaiter。
awaiter3 = default; // 清除 Awaiter 的引用。
state = -1; // 重置状态为 -1,表示状态机当前未挂起。
goto Case_FetchCompleted; // 跳转到 FetchDataAsync 完成后的逻辑。
...
Case_FetchCompleted: // FetchDataAsync 操作完成,处理结果
tempHtmlResult = stringTaskAwaiter.GetResult(); // 获取 FetchDataAsync 的返回结果(HTML 数据)。
html = tempHtmlResult; // 将结果赋值给 html 变量。
tempHtmlResult = null; // 清理临时变量。
// 调用 ProcessDataAsync 方法以处理 HTML 数据,并获取其 Awaiter。
stringTaskAwaiter = ProcessDataAsync(html).GetAwaiter();
if (!stringTaskAwaiter.IsCompleted) // 如果异步操作尚未完成,则需要挂起状态机。
{
state = 1; // 将状态设置为 1,表示挂起点在 ProcessDataAsync 处。
awaiter2 = stringTaskAwaiter; // 保存当前的 Awaiter(对应 ProcessDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存当前状态机实例。
// 挂起状态机,并在异步操作完成后恢复执行。
taskBuilder.AwaitUnsafeOnCompleted(ref stringTaskAwaiter, ref stateMachine);
return; // 退出方法,等待异步操作完成时重新进入。
}
重点解读:
- 先重置一下状态
- 然后通过
stringTaskAwaiter.GetResult()
获取上一个异步操作的执行结果 - 然后启动另一个操作
ProcessDataAsync
- 将状态机的状态从-1设置成1。
- 然后执行
taskBuilder.AwaitUnsafeOnCompleted
然后又返回了。
第四步:启动异步方法SaveDataAsync
当状态机的状态是1
时候,MoveNext执行如下内容:
case 1: // 从 ProcessDataAsync 挂起点恢复
stringTaskAwaiter = awaiter2; // 恢复之前保存的 Awaiter。
awaiter2 = default; // 清除 Awaiter 的引用。
state = -1; // 重置状态为 -1,表示状态机当前未挂起。
goto Case_ProcessCompleted; // 跳转到 ProcessDataAsync 完成后的逻辑。
...
Case_ProcessCompleted: // ProcessDataAsync 操作完成,处理结果
tempProcessedResult = stringTaskAwaiter.GetResult(); // 获取 ProcessDataAsync 的返回结果(处理后的 HTML 数据)。
processedHtml = tempProcessedResult; // 将结果赋值给 processedHtml 变量。
tempProcessedResult = null; // 清理临时变量。
// 调用 SaveDataAsync 方法以保存处理后的数据,并获取其 Awaiter。
simpleAwaiter = SaveDataAsync(processedHtml).GetAwaiter();
if (!simpleAwaiter.IsCompleted) // 如果异步操作尚未完成,则需要挂起状态机。
{
state = 2; // 将状态设置为 2,表示挂起点在 SaveDataAsync 处。
awaiter = simpleAwaiter; // 保存当前的 Awaiter(对应 SaveDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存当前状态机实例。
// 挂起状态机,并在异步操作完成后恢复执行。
taskBuilder.AwaitUnsafeOnCompleted(ref simpleAwaiter, ref stateMachine);
return; // 退出方法,等待异步操作完成时重新进入。
}
重点解读:
- 重置状态
- 然后通过
stringTaskAwaiter.GetResult()
获取上一个异步操作的执行结果 - 然后启动最后一个操作
SaveDataAsync
- 将状态机的状态从-1设置成2。
- 然后执行
taskBuilder.AwaitUnsafeOnCompleted
然后又返回了。
第五步:状态机执行完毕,设置异步方法的最终结果
当状态机的状态是2
时候,MoveNext执行如下内容:
public void MoveNext()
{
...
try
{
...
switch (currentState)
{
...
case 2: // 从 SaveDataAsync 挂起点恢复
simpleAwaiter = awaiter; // 恢复之前保存的 Awaiter。
awaiter = default; // 清除 Awaiter 的引用。
state = -1; // 重置状态为 -1,表示状态机当前未挂起。
break;
...
}
// SaveDataAsync 操作完成,确保任务成功结束
simpleAwaiter.GetResult(); // 调用 GetResult 确保 SaveDataAsync 没有抛出异常。
}
catch (Exception exception) // 捕获异步操作中可能抛出的任何异常
{
state = -2; // 将状态机的状态设置为 -2,表示已完成且发生异常。
taskBuilder.SetException(exception); // 将捕获的异常传递给 TaskBuilder,通知调用方任务失败。
return; // 退出方法,状态机终止。
}
// 异步任务成功完成
state = -2; // 将状态机的状态设置为 -2,表示已完成且没有异常。
taskBuilder.SetResult(); // 标记任务完成并通知调用方。
}
重点解读:
- 重置状态,然后
break;
,跳出了switch
- 将状态机的状态设置成-2:
state = -2;
表示整个状态机执行结束。 - 最后设置整个异步任务的结果,由于
SaveDataFromUrlAsync
返回的是个Task,任务不包含具体的返回值所以是taskBuilder.SetResult()
,否则是taskBuilder.SetResult(result)
。
小结
简单总结一下:
- 状态机的
MoveNext
方法会根据其不同状态执行不同的代码段 - 每一段会启动一个异步操作,对应的就是原异步方法中的
await
后面的异步操作。 - state为-1之后的每次
MoveNext
调用,都会先获取前一个异步操作的执行结果。
至此,异步方法对应的整个状态机的大致执行过程咱们也搞明白了,但还剩一点没搞明白,就是第一次MoveNext
执行后,就就返回了,后续状态的MoveNext
在哪儿执行?什么时候执行?这就要看builder.AwaitUnsafeOnCompleted
到底做了什么了,builder.AwaitUnsafeOnCompleted的内部咱们还是一头雾水,咱们还不知道。
3.AsyncTaskMethodBuilder<T>.AwaitUnsafeOnCompleted
源码解读
完整源码
其中AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted涉及到的源码github链接:
AsyncTaskMethodBuilderT.cs:
- AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine, [NotNull] ref Task
? taskField) - GetStateMachineBox
- AwaitUnsafeOnCompleted
(ref TAwaiter awaiter, IAsyncStateMachineBox box)
TaskAwaiter.cs:
Task.cs:
- UnsafeSetContinuationForAwait(IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext)
- AddTaskContinuation(object tc, bool addBeforeOthers)
- AddTaskContinuationComplex(object tc, bool addBeforeOthers)
解读
让我们详细解读并简化这段代码,同时强调关键点,以深入理解整个AwaitUnsafeOnCompleted方法的核心逻辑
整体概述
AwaitUnsafeOnCompleted
是异步状态机的核心部分,它实现了异步方法的挂起和恢复。以下是它的主要职责:
- 捕获状态机和上下文:将当前的状态机(
stateMachine
)和上下文(ExecutionContext
或同步上下文)绑定到一个包装器(IAsyncStateMachineBox
),以便在异步任务完成后继续执行。 - 注册回调:将状态机的
MoveNext
操作注册为任务完成后的回调。 - 调度执行:根据上下文(比如
SynchronizationContext
或TaskScheduler
),决定在任务完成后如何恢复回调的执行。 - 处理异常:在挂起和恢复过程中捕获并处理可能的异常。
分步解析
1. 顶层方法:AwaitUnsafeOnCompleted
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine =>
AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task);
功能:
- 这是一个公开的方法,供编译器生成的状态机代码调用。它的作用是:
- 将
awaiter
(表示异步操作的等待器)和stateMachine
(当前异步方法的状态机)传递给内部实现。 m_task
是当前异步方法的Task
,用于跟踪异步方法的状态。
- 将
关键点:
TAwaiter
必须实现ICriticalNotifyCompletion
,这是所有awaiter
(如TaskAwaiter
或ValueTaskAwaiter
)的基础接口。TStateMachine
必须实现IAsyncStateMachine
,这是所有异步状态机的基础接口。
2. 内部实现:AwaitUnsafeOnCompleted
internal static void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine, [NotNull] ref Task<TResult>? taskField)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
// 将 stateMachine 包装成一个 IAsyncStateMachineBox 对象,用于存储状态机、任务和上下文信息。
IAsyncStateMachineBox box = GetStateMachineBox(ref stateMachine, ref taskField);
// 通过传递 awaiter 和 box 连接状态机与 awaiter,最终将状态机的 MoveNext 方法注册为任务完成后的回调。
AwaitUnsafeOnCompleted(ref awaiter, box);
}
功能:
- 这是
AwaitUnsafeOnCompleted
的内部实现。它主要完成两个任务:- 获取状态机的包装器:调用
GetStateMachineBox
方法,将状态机(stateMachine
)与当前任务(taskField
)绑定到一个IAsyncStateMachineBox
对象中。 - 注册回调:调用另一个重载的
AwaitUnsafeOnCompleted
方法,将任务完成后的回调注册到awaiter
。
- 获取状态机的包装器:调用
关键点:
IAsyncStateMachineBox
是状态机的包装器,用于保存上下文和状态机的执行逻辑(如MoveNext
方法)。- 任务(
taskField
)和状态机的绑定使得异步方法的执行状态可以被跟踪和恢复。
3. 包装状态机:GetStateMachineBox
// 封装盒是一个特殊的数据结构,用于管理异步方法的执行状态
private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
ref TStateMachine stateMachine, // 状态机实例,用于跟踪异步方法的执行状态
[NotNull] ref Task<TResult>? taskField) // 可空的 Task<TResult> 引用,用于存储任务结果
where TStateMachine : IAsyncStateMachine // TStateMachine 必须实现 IAsyncStateMachine 接口
{
// 捕获当前的 ExecutionContext(执行上下文)
// 用于将当前线程的上下文(如同步上下文、文化信息等)传递给异步操作
ExecutionContext? currentContext = ExecutionContext.Capture();
IAsyncStateMachineBox result; // 定义返回值,封装当前的状态机实例
// 检查 taskField 是否已经是一个强类型的 AsyncStateMachineBox<TStateMachine>
if (taskField is AsyncStateMachineBox<TStateMachine> stronglyTypedBox)
{
// 如果封装盒的上下文与当前上下文不同,则更新上下文
if (stronglyTypedBox.Context != currentContext)
{
stronglyTypedBox.Context = currentContext;
}
// 将封装盒赋值给 result
result = stronglyTypedBox;
}
else
{
// 如果 taskField 为空或不是正确的封装盒,则创建一个新的封装盒
AsyncStateMachineBox<TStateMachine> box = new AsyncStateMachineBox<TStateMachine>();
// 将新的封装盒赋值给 taskField,这样外部代码可以访问到任务的状态
taskField = box;
// 初始化封装盒的状态机和上下文
box.StateMachine = stateMachine;
box.Context = currentContext;
// 将封装盒赋值给 result
result = box;
}
// 返回封装盒
return result;
}
功能:
- 负责将状态机(
stateMachine
)与任务(taskField
)绑定到状态机包装器(IAsyncStateMachineBox
)。 - 捕获上下文:通过
ExecutionContext.Capture
捕获当前上下文(如同步上下文、线程上下文等),以便任务完成后能正确恢复上下文。
关键点:
- 如果任务已经存在且是强类型的是一个强类型的
AsyncStateMachineBox<TStateMachine>
,则直接复用;如果任务尚未创建,则创建新的AsyncStateMachineBox
。 - 状态机包装器的核心是将状态机的
MoveNext
方法与上下文绑定到一起。
4. 注册回调:AsyncTaskMethodBuilderT
的 AwaitUnsafeOnCompleted
重载方法
// 该方法负责将状态机与 Awaiter 关联起来,并注册异步操作完成后的回调
internal static void AwaitUnsafeOnCompleted<TAwaiter>(
ref TAwaiter awaiter, // 异步操作的 Awaiter,用于等待异步结果
IAsyncStateMachineBox box) // 状态机封装盒,存储状态并控制方法的执行流程
where TAwaiter : ICriticalNotifyCompletion // TAwaiter 必须实现 ICriticalNotifyCompletion 接口
{
// 检查 Awaiter 是否实现了 ITaskAwaiter 接口(通常是标准的 TaskAwaiter)
if (awaiter is ITaskAwaiter taskAwaiter)
{
// 如果是 TaskAwaiter,则调用 UnsafeOnCompletedInternal,关联任务和状态机
// continueOnCapturedContext 参数设置为 true,表示继续在捕获的上下文中执行后续操作
TaskAwaiter.UnsafeOnCompletedInternal(
taskAwaiter.m_task, // 等待的底层任务
box, // 包装器,包含状态机和回调。
continueOnCapturedContext: true // 指定回调(MoveNext)是否在捕获的同步上下文中恢复
);
}
// 检查 Awaiter 是否实现了 IConfiguredTaskAwaiter 接口(支持配置的 Awaiter)
else if (awaiter is IConfiguredTaskAwaiter configuredAwaiter)
{
// 如果是 ConfiguredTaskAwaiter,则根据其配置决定上下文捕获行为
TaskAwaiter.UnsafeOnCompletedInternal(
configuredAwaiter.m_task, // 等待的底层任务
box, // 包装器,包含状态机和回调。
(configuredAwaiter.m_options & ConfigureAwaitOptions.ContinueOnCapturedContext) != 0
// 判断配置是否要求回调(MoveNext)继续在捕获的上下文中执行
);
}
else
{
// 对于其他类型的 Awaiter,直接调用其 UnsafeOnCompleted 方法
// 注册状态机的 MoveNextAction 作为回调,当异步操作完成时执行
awaiter.UnsafeOnCompleted(box.MoveNextAction);
}
}
功能:
- 负责将状态机的回调(
box.MoveNextAction
)注册到awaiter
,以便在任务完成后继续执行状态机。 - 根据
awaiter
的不同类型,如TaskAwaiter
、ConfiguredTaskAwaiter
(配置过的Awaiter
,比如调用过.ConfigureAwait(false)
),选择不同的方式注册回调。
关键点:
- 如果
awaiter
是TaskAwaiter
,调用TaskAwaiter.UnsafeOnCompletedInternal
。 - 如果是其他类型(如
ValueTaskAwaiter
),直接调用UnsafeOnCompleted
。
5. 注册回调:TaskAwaiter
的UnsafeOnCompletedInternal
internal static void UnsafeOnCompletedInternal(
Task task, // 等待的任务实例。
IAsyncStateMachineBox stateMachineBox, // 异步状态机的包装器,包含状态机的引用及其回调。
bool continueOnCapturedContext // 是否需要在捕获的上下文中恢复执行。
)
{
// 检查是否启用了调试/事件跟踪功能
if (TplEventSource.Log.IsEnabled() || Task.s_asyncDebuggingEnabled)
{
// 如果启用了事件日志或异步调试,调用 SetContinuationForAwait 方法。
// 参数说明:
// - stateMachineBox.MoveNextAction: 状态机的回调方法(MoveNext)将作为任务完成后的延续。
// - continueOnCapturedContext: 是否需要在捕获的上下文中恢复执行。
// - flowExecutionContext: false 表示不需要传递 ExecutionContext。
task.SetContinuationForAwait(stateMachineBox.MoveNextAction, continueOnCapturedContext, flowExecutionContext: false);
}
else
{
// 如果没有启用调试/事件跟踪,调用 UnsafeSetContinuationForAwait 方法。
// 参数说明:
// - stateMachineBox: 包装器本身,包含状态机的引用和回调。
// - continueOnCapturedContext: 是否需要在捕获的上下文中恢复执行。
task.UnsafeSetContinuationForAwait(stateMachineBox, continueOnCapturedContext);
}
}
功能:
- 将状态机的回调(
stateMachineBox.MoveNextAction
)注册到任务(task
),以便任务完成后调用状态机的MoveNext
方法。 - 如果启用了调试或事件跟踪(
TplEventSource.Log.IsEnabled
),会记录调试信息。 - 如果没有调试需求,则直接调用
UnsafeSetContinuationForAwait
。
6. 包装回调对象并将其添加到任务延续中:Task.UnsafeSetContinuationForAwait
// 用于为当前任务设置异步操作完成后的状态机回调。
// 根据上下文的捕获情况决定如何调度回调的执行。
internal void UnsafeSetContinuationForAwait(
IAsyncStateMachineBox stateMachineBox, // 异步状态机的包装器,包含状态机以及完成后的回调。
bool continueOnCapturedContext // 是否需要在捕获的上下文中恢复执行。
)
{
// 如果需要在捕获的上下文中恢复执行
if (continueOnCapturedContext)
{
// 检查当前线程是否有 SynchronizationContext
if (SynchronizationContext.Current is SynchronizationContext syncCtx
&& syncCtx.GetType() != typeof(SynchronizationContext)) // 确保 SynchronizationContext 是自定义实现
{
// 如果存在自定义同步上下文(非默认同步上下文),
// 创建一个针对同步上下文的异步任务延续对象
var tc = new SynchronizationContextAwaitTaskContinuation(
syncCtx, // 当前的 SynchronizationContext,用于调度回调。
stateMachineBox.MoveNextAction, // 状态机的 MoveNext 方法,作为延续回调。
flowExecutionContext: false // 表示不传递 ExecutionContext。
);
// 将任务延续注册到当前任务中。
// 参数:
// - tc: 延续对象。
// - addBeforeOthers: false,表示将延续添加到任务队列的末尾。
AddTaskContinuation(tc, addBeforeOthers: false);
}
// 检查当前是否有自定义 TaskScheduler(并且不是默认的 TaskScheduler)
else if (TaskScheduler.InternalCurrent is TaskScheduler scheduler && scheduler != TaskScheduler.Default)
{
// 如果存在自定义任务调度器,
// 创建一个针对任务调度器的异步任务延续对象
var tc = new TaskSchedulerAwaitTaskContinuation(
scheduler, // 当前的 TaskScheduler,用于调度回调。
stateMachineBox.MoveNextAction, // 状态机的 MoveNext 方法,作为延续回调。
flowExecutionContext: false // false,表示不传递 ExecutionContext。
);
// 将任务延续注册到当前任务中。
// 参数:
// - tc: 延续对象。
// - addBeforeOthers: false,表示将延续添加到任务队列的末尾。
AddTaskContinuation(tc, addBeforeOthers: false);
}
// 如果没有自定义 SynchronizationContext 或 TaskScheduler
else
{
// 直接将状态机包装器作为任务的延续。
// 参数:
// - stateMachineBox: 包含任务完成后的回调。
// - addBeforeOthers: false,表示将延续添加到任务队列的末尾。
AddTaskContinuation(stateMachineBox, addBeforeOthers: false);
}
}
// 如果不需要在捕获的上下文中恢复执行
else
{
// 直接将状态机包装器作为任务的延续。
AddTaskContinuation(stateMachineBox, addBeforeOthers: false);
}
}
功能:
- 检查当前的上下文(
SynchronizationContext
或TaskScheduler
),决定在任务完成后如何调度状态机的执行。 - 需要上下文切换:
- 如果存在
SynchronizationContext
且不是默认实现,则将当前同步上下文和回调(MoveNextAction
)包装成一个同步上下文任务延续(SynchronizationContextAwaitTaskContinuation)
,然后添加到任务的延续(回调)中。 - 如果存在自定义的
TaskScheduler
,则将当前调度器和回调(MoveNextAction
)包装成一个调度器任务延续(TaskSchedulerAwaitTaskContinuation)
,然后添加到任务的延续(回调)中。
- 如果存在
- 无需上下文切换:
- 如果没有上下文要求,则直接将状态机的包装器(
IAsyncStateMachineBox
)作为回调,并通过AddTaskContinuation
注册。
- 如果没有上下文要求,则直接将状态机的包装器(
上下文调度机制:
- 如果使用
SynchronizationContextAwaitTaskContinuation
,最终会调用syncCtx.Post(MoveNextAction)
,以确保回调在捕获的上下文中执行。 - 如果使用
TaskSchedulerAwaitTaskContinuation
,回调将通过指定的任务调度器调度。
详细了解调度,请参考:第十三章:调度
7. 注册回调到任务中:AddTaskContinuation
private bool AddTaskContinuation(object tc, bool addBeforeOthers)
{
Debug.Assert(tc != null);
// 如果任务已经完成,则无法继续添加回调,直接返回 false
if (IsCompleted) return false;
// 尝试将回调对象直接存储到 m_continuationObject 字段中
if ((m_continuationObject != null) || (Interlocked.CompareExchange(ref m_continuationObject, tc, null) != null))
{
// 如果 m_continuationObject 已经有值,则进入复杂逻辑
return AddTaskContinuationComplex(tc, addBeforeOthers);
}
else
{
// 如果成功将回调存储到 m_continuationObject 中,返回 true
return true;
}
}
功能:
-
检查任务完成状态:
- 如果任务已经完成,则无法继续添加回调,直接返回
false
,表示添加失败。
- 如果任务已经完成,则无法继续添加回调,直接返回
-
快速存储单个回调:
- 如果当前
m_continuationObject
为空,则通过Interlocked.CompareExchange
尝试将回调对象tc
原子性地存储到m_continuationObject
中。 - 如果存储成功,表示此任务仅有一个回调,返回
true
。
- 如果当前
-
多回调处理:
- 如果
m_continuationObject
已经存在值,则调用AddTaskContinuationComplex
,将回调列表化并处理多回调的逻辑。
- 如果
8. 多回调处理:AddTaskContinuationComplex
private bool AddTaskContinuationComplex(object tc, bool addBeforeOthers)
{
Debug.Assert(tc != null, "Expected non-null tc object in AddTaskContinuationComplex");
object? oldValue = m_continuationObject;
Debug.Assert(oldValue is not null, "Expected non-null m_continuationObject object");
// 如果任务已经完成,则无法添加回调,直接返回 false
if (oldValue == s_taskCompletionSentinel)
{
return false;
}
// 如果当前只存储了单个回调对象,则将其转换为回调列表
List<object?>? list = oldValue as List<object?>;
if (list is null)
{
// 构造一个新的回调列表,将旧回调和新的回调一起存储
list = new List<object?>();
if (addBeforeOthers)
{
list.Add(tc);
list.Add(oldValue);
}
else
{
list.Add(oldValue);
list.Add(tc);
}
// 尝试将回调列表存储到 m_continuationObject 中
object? expected = oldValue;
oldValue = Interlocked.CompareExchange(ref m_continuationObject, list, expected);
if (oldValue == expected)
{
// 如果存储成功,返回 true
return true;
}
// 如果存储失败,重新检查 m_continuationObject 的状态
list = oldValue as List<object?>;
if (list is null)
{
Debug.Assert(oldValue == s_taskCompletionSentinel, "Expected m_continuationObject to be list or sentinel");
return false;
}
}
// 如果 m_continuationObject 已经是一个回调列表,则直接将新的回调添加到列表中
lock (list)
{
// 如果任务已经完成,则无法添加回调,返回 false
if (m_continuationObject == s_taskCompletionSentinel)
{
return false;
}
// 清理列表中的空条目(可能是由于移除操作导致的)
if (list.Count == list.Capacity)
{
list.RemoveAll(l => l == null);
}
// 根据 `addBeforeOthers` 参数决定将新的回调添加到列表的头部或尾部
if (addBeforeOthers)
{
list.Insert(0, tc);
}
else
{
list.Add(tc);
}
}
return true; // 回调成功添加到列表中
}
功能:
-
检查任务完成状态:
- 如果
m_continuationObject
是s_taskCompletionSentinel
(表示任务已完成),则无法添加新的回调。
- 如果
-
将单回调转换为列表:
- 如果当前
m_continuationObject
只存储了单个回调,则将其转换为回调列表,并将新回调一同存储。
- 如果当前
-
多回调列表的处理:
- 如果
m_continuationObject
已经是一个回调列表,则直接将新回调添加到列表中。 - 根据
addBeforeOthers
参数,决定将新的回调添加到头部或尾部。
- 如果
-
线程安全:
- 使用
Interlocked.CompareExchange
和lock
确保多线程环境下的操作安全。
- 使用
总结与重点
-
核心目标:
AwaitUnsafeOnCompleted
的核心目标是将状态机的MoveNext
方法注册为任务完成后的回调,并根据捕获的上下文/调度器
及要求决定回调的执行是否需要调度到对应的上下文/调度器
。- 通过
UnsafeSetContinuationForAwait
,异步状态机能够在任务完成后恢复执行,且始终在正确的上下文中运行。
-
状态机包装器:
- 使用
GetStateMachineBox
创建或获取IAsyncStateMachineBox
,将状态机与任务绑定。 - 包装器会捕获状态机的
MoveNext
方法和当前执行上下文。
- 使用
-
回调注册:
UnsafeSetContinuationForAwait
调用AddTaskContinuation
,将状态机的回调注册到任务。- 如果任务已经完成,回调会立即触发;否则,等待任务完成后触发。
-
上下文切换:
- 根据
SynchronizationContext
或TaskScheduler
决定回调的执行是否需要切换上下文。 - 如果需要上下文切换,会通过上下文调度器(如
SynchronizationContext.Post
或TaskScheduler.QueueTask
)确保回调在正确的环境中执行。
- 根据
-
调试支持:
- 在调试模式下,通过
TplEventSource.Log
添加事件跟踪和任务活动记录。
- 在调试模式下,通过
4. await 的核心
await
的本质是对一个实现了 Awaiter 模式 的对象进行操作。Awaiter 模式由以下方法和属性组成:
-
GetAwaiter
方法:
返回一个Awaiter
对象,该对象必须实现以下方法和属性。 -
IsCompleted
属性:
表示异步操作是否已经完成。 -
OnCompleted
方法:
注册一个回调,当异步操作完成时调用。 -
GetResult
方法:
获取异步操作的结果。如果任务失败,会抛出异常。
3.1 Awaiter 模式的实现
以下是一个简化的 Awaiter 示例:
public class MyAwaiter : INotifyCompletion
{
private Task _task;
public MyAwaiter(Task task)
{
_task = task;
}
public bool IsCompleted => _task.IsCompleted;
public void OnCompleted(Action continuation)
{
_task.ContinueWith(_ => continuation());
}
public void GetResult()
{
_task.Wait(); // 等待任务完成
}
}
当我们调用 await
时,编译器会将代码拆解为类似以下形式:
var awaiter = myAwaitable.GetAwaiter(); // 获取 Awaiter
if (!awaiter.IsCompleted) // 如果任务尚未完成
{
awaiter.OnCompleted(() => MoveNext()); // 注册回调
return; // 暂停当前方法
}
awaiter.GetResult(); // 获取任务结果或抛出异常
3.2 SynchronizationContext 的作用
在使用 await
时,默认情况下,恢复操作会在捕获的上下文中执行,例如:
-
UI 应用(WPF/WinForms):
恢复操作会切换回主线程,以便更新 UI。 -
ASP.NET Core:
默认没有捕获上下文,恢复操作直接在线程池中执行。
SynchronizationContext
是负责上下文切换的核心组件。await
会调用 SynchronizationContext.Post
方法,将后续代码调度到适当的线程。
3.3 为什么 ConfigureAwait(false)
可以禁用上下文捕获?
默认情况下,await
会捕获当前的 SynchronizationContext
,以便在异步操作完成后切换回原来的上下文。但在某些场景(例如后台服务或性能敏感的代码中),这种切换可能是多余的。
通过调用 ConfigureAwait(false)
,可以禁用上下文捕获,直接在线程池中执行后续代码:
await SomeAsyncMethod().ConfigureAwait(false);
具体原因参考4. 注册回调:AsyncTaskMethodBuilderT
的 AwaitUnsafeOnCompleted
这样可以避免上下文切换带来的性能开销,但需要注意禁用上下文捕获后,后续操作在线程池线程上执行,因此无法直接更新 UI。
14.4 自定义Task、async/await实现
上一节通过在源码层面深入解读了async
/await
的原理,以及 Awaiter
模式,本节通过自定义 Task
和Awaiter
模式来加深对async
/await
的理解。
自定义Task:CustomTask
using System;
using System.Threading;
namespace SimpleTest;
// 自定义Task
public class CustomTask<T>
{
private T _result; // 保存任务结果
private Exception? _exception; // 保存任务中的异常
private bool _isCompleted; // 是否已完成
// 将 _continuation 的访问修饰符改为 internal
internal Action? _continuation; // 异步完成后的回调
private readonly object _lock = new(); // 用于线程安全的锁
// 构造函数:接受一个工作委托并启动
public CustomTask(Func<T> work)
{
if (work == null) throw new ArgumentNullException(nameof(work)); // 确保工作委托不为 null
// 启动一个线程来执行任务
new Thread(() =>
{
try
{
_result = work(); // 执行任务并保存结果
}
catch (Exception ex)
{
_exception = ex; // 捕获异常
}
finally
{
SetCompleted(); // 标记任务完成
}
}).Start();
}
// 启动一个自定义Task
public static CustomTask<T> Run(Func<T> work)
{
return new CustomTask<T>(work);
}
// 设置任务为完成状态,并触发回调
private void SetCompleted()
{
lock (_lock)
{
_isCompleted = true; // 标记任务已完成
_continuation?.Invoke(); // 如果有回调,立即调用
}
}
// 获取自定义 Awaiter 实例
public CustomAwaiter<T> GetAwaiter()
{
return new CustomAwaiter<T>(this, continueOnCapturedContext: true);
}
// 提供 ConfigureAwait 功能
public ConfiguredCustomAwaiter ConfigureAwait(bool continueOnCapturedContext)
{
return new ConfiguredCustomAwaiter(this, continueOnCapturedContext);
}
// 任务状态属性
public bool IsCompleted
{
get
{
lock (_lock) return _isCompleted;
}
}
// 获取任务结果
public T Result
{
get
{
lock (_lock)
{
if (!_isCompleted)
{
throw new InvalidOperationException("任务尚未完成,无法获取结果。");
}
if (_exception != null)
{
throw _exception; // 如果任务失败,抛出异常
}
return _result; // 返回结果
}
}
}
// 内部结构体:用于支持 ConfigureAwait 功能
public readonly struct ConfiguredCustomAwaiter
{
private readonly CustomTask<T> _task;
private readonly bool _continueOnCapturedContext;
public ConfiguredCustomAwaiter(CustomTask<T> task, bool continueOnCapturedContext)
{
_task = task;
_continueOnCapturedContext = continueOnCapturedContext;
}
public CustomAwaiter<T> GetAwaiter()
{
return new CustomAwaiter<T>(_task, _continueOnCapturedContext);
}
}
}
自定义Awaiter:CustomAwaiter
using System;
using System.Runtime.CompilerServices;
using System.Threading;
namespace SimpleTest;
// 自定义 Awaiter
public class CustomAwaiter<T> : INotifyCompletion
{
private readonly CustomTask<T> _task; // 任务实例
private readonly bool _continueOnCapturedContext; // 是否继续捕获同步上下文
public CustomAwaiter(CustomTask<T> task, bool continueOnCapturedContext)
{
_task = task ?? throw new ArgumentNullException(nameof(task)); // 确保任务不为 null
_continueOnCapturedContext = continueOnCapturedContext; // 保存上下文捕获设置
}
// 属性:任务是否完成
public bool IsCompleted => _task.IsCompleted;
// 方法:注册异步完成后的回调
public void OnCompleted(Action continuation)
{
if (continuation == null) throw new ArgumentNullException(nameof(continuation)); // 确保回调不为 null
if (_task.IsCompleted)
{
continuation(); // 如果任务已完成,直接调用回调
}
else
{
// 保存回调,在任务完成时调用
lock (_task)
{
if (_task.IsCompleted)
{
continuation(); // 避免竞争条件,检查状态后再调用
}
else
{
_task._continuation += () =>
{
if (_continueOnCapturedContext)
{
var syncContext = SynchronizationContext.Current; // 获取当前的同步上下文
if (syncContext != null)
{
syncContext.Post(_ => continuation(), null); // 在同步上下文中调度回调
return;
}
}
continuation(); // 不捕获上下文时,直接调用回调
};
}
}
}
}
// 方法:获取任务结果
public T GetResult()
{
return _task.Result; // 返回任务结果
}
}
使用自定义的Task
和Awaiter
static async void TestCustomTaskAsync()
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 任务开始...");
// 使用 CustomTask.Run 启动一个任务
var customTask = CustomTask<int>.Run(() =>
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 任务执行中...");
Thread.Sleep(2000); // 模拟耗时操作
return 42; // 返回计算结果
});
// 使用 ConfigureAwait(false) 防止捕获同步上下文
int result = await customTask.ConfigureAwait(false);
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 任务完成,结果:{result}");
}
TestCustomTaskAsync();
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 主线程继续执行...");
Thread.Sleep(3000); // 确保异步任务完成
14.5 async/await 为什么这么好用?
async/await
是 .NET 中异步编程的核心,它提供了一种简洁、直观的方式来编写异步代码,同时解决了传统异步编程中的许多痛点。本节将从以下几个方面探讨 async/await
的优势。
1. 同步方式编写异步代码
1.1 如何避免回调地狱?
在传统异步模型中,回调函数(callback)是异步操作的主要手段。例如,JavaScript 中的异步操作曾经大量依赖回调函数。这种方式虽然有效,但容易导致“回调地狱”(callback hell),使代码变得难以阅读和维护:
// 传统回调式代码(伪代码)
DoSomethingAsync(result1 =>
{
DoSomethingElseAsync(result1, result2 =>
{
FinalStepAsync(result2, finalResult =>
{
Console.WriteLine(finalResult);
});
});
});
使用 async/await
后,异步代码可以像同步代码一样按顺序书写,大大提升了代码的可读性和可维护性:
// 使用 async/await
var result1 = await DoSomethingAsync();
var result2 = await DoSomethingElseAsync(result1);
var finalResult = await FinalStepAsync(result2);
Console.WriteLine(finalResult);
优势:
- 消除了嵌套: 每一行代码表示一个明确的操作,逻辑清晰,不再需要过多的闭包和嵌套结构。
- 更易维护: 当业务逻辑改变时,只需调整对应的逻辑代码,而不用修改复杂的回调链。
- 更贴近同步逻辑: 异步代码的执行方式接近于同步代码,开发者无需掌握复杂的异步编程模型。
但目前 async/await 的根本原理还是基于回调,但已经被编译器隐藏起来了。
1.2 代码的可读性和可维护性如何得到提升?
-
从事件驱动到线性逻辑:
async/await
将事件驱动的异步编程简化为线性顺序的代码逻辑,开发者无需显式管理回调函数。 -
状态管理自动化:
使用传统异步方式时,开发者需要显式保存和恢复状态(例如通过闭包)。而async/await
自动生成状态机,帮助保存方法的执行上下文和异步操作的执行状态。 -
异常处理统一:
异步代码的异常可以通过try/catch
捕获,不需要显式处理每个回调的错误,减少了大量的错误处理代码。
示例:传统异步 vs async/await
传统异步代码:
DoSomethingAsync(
success =>
{
if (success)
{
DoSomethingElseAsync(
result =>
{
if (result == null)
{
HandleError("Result is null");
return;
}
Console.WriteLine("Success!");
},
error =>
{
HandleError(error);
});
}
else
{
HandleError("DoSomethingAsync failed");
}
},
error =>
{
HandleError(error);
});
使用 async/await
后:
try
{
bool success = await DoSomethingAsync();
if (!success) throw new Exception("DoSomethingAsync failed");
var result = await DoSomethingElseAsync();
if (result == null) throw new Exception("Result is null");
Console.WriteLine("Success!");
}
catch (Exception ex)
{
HandleError(ex.Message);
}
总结:
async/await
提供了接近同步代码的写法,减少了嵌套、状态管理和显式回调,使代码更易于阅读和维护。
2. 错误处理
2.1 异常传播机制:为什么可以用 try/catch 捕获异步代码中的异常?
在 async/await
的实现中,异常传播遵循以下规则:
- 当异步方法中抛出异常时,异常会被捕获并存储在返回的
Task
对象中。 - 如果在调用异步方法时使用了
await
,异常会在await
的位置重新抛出。 - 异常传播的机制使得我们可以像同步代码一样使用
try/catch
捕获异步方法中的异常。
示例:
async Task<int> DivideAsync(int a, int b)
{
if (b == 0) throw new DivideByZeroException("Cannot divide by zero");
return a / b;
}
async Task TestAsync()
{
try
{
int result = await DivideAsync(10, 0);
Console.WriteLine($"Result: {result}");
}
catch (Exception ex)
{
Console.WriteLine($"Caught exception: {ex.Message}");
}
}
输出:
Caught exception: Cannot divide by zero
2.2 Task 的 AggregateException 与 await 异常传播的关系
如果直接访问 Task
的结果(例如通过 Task.Result
或 Task.Wait
),异常会被包装在 AggregateException
中:
try
{
var task = DivideAsync(10, 0);
task.Wait(); // 或 task.Result
}
catch (AggregateException aggEx)
{
foreach (var ex in aggEx.InnerExceptions)
{
Console.WriteLine($"Caught exception: {ex.Message}");
}
}
但是,如果使用 await
,异常会自动从 AggregateException
中提取出来并直接抛出,更符合开发者的期望:
try
{
int result = await DivideAsync(10, 0);
}
catch (Exception ex)
{
Console.WriteLine($"Caught exception: {ex.Message}");
}
总结:
async/await
自动提取异常,避免了开发者处理 AggregateException
的复杂性,使异常处理更加直观。
3. 性能优化
3.1 为什么 async/await 的性能比传统异步模型更优?
传统异步模型(如基于线程的异步编程)常常需要显式创建和管理线程、上下文切换以及状态保存,这些操作开销较大。而 async/await
基于以下优化实现高效的异步操作:
-
基于状态机的轻量开销:
async/await
编译器会为每个异步方法生成一个隐式的状态机,负责管理方法的执行状态。- 状态机的创建和调度开销远低于显式线程的创建和切换。
-
任务复用:
- 异步方法只有在真正遇到异步操作(例如
await
)时,才会挂起并返回控制权。未使用await
的异步方法会直接同步执行,避免不必要的性能开销。
- 异步方法只有在真正遇到异步操作(例如
-
线程池的高效利用:
async/await
不会阻塞线程。方法挂起时,线程可以被释放,用于处理其他任务。- 对于 I/O 操作,
async/await
会使用操作系统底层的异步 API,避免线程的占用。
3.2 基于状态机的轻量开销
编译器会将每个 async
方法转换为一个隐式的状态机。这个状态机负责:
- 保存异步方法的执行状态(例如当前执行到哪一步)。
- 在异步操作完成后,恢复执行。
示例:简单的 async
方法状态机
async Task<int> SampleAsync()
{
await Task.Delay(1000);
return 42;
}
编译器生成的状态机伪代码:
struct SampleAsyncStateMachine : IAsyncStateMachine
{
public int State; // 保存当前状态
public AsyncTaskMethodBuilder<int> Builder; // 构建器
public void MoveNext()
{
switch (State)
{
case 0:
Task.Delay(1000).ContinueWith(()=>MoveNext());
State = 1;
return;
case 1:
Builder.SetResult(42);
return;
}
}
}
3.3 异步方法的延迟执行与线程池的高效利用
async/await
的设计避免了不必要的线程创建:
- 异步方法只有在遇到
await
时才会挂起。如果异步方法没有await
,它会像普通方法一样同步执行。 - 挂起后,线程会被释放,避免线程阻塞,提高线程池的利用率。
总结
async/await
的强大之处在于:
- 同步方式编写异步代码: 简化了异步编程逻辑,消除了回调地狱。
- 错误处理: 提供了清晰的异常传播机制,支持统一的
try/catch
异常处理。 - 性能优化: 基于状态机的轻量实现,结合线程池和延迟执行,提供了高效的异步性能。
这些特性使得 async/await
成为 .NET 中异步编程的首选工具,大幅提升了开发效率和代码质量。
14.6 async/await 与同步上下文的协作
SynchronizationContext
负责在异步操作完成后,决定代码的执行上下文(例如线程、特定的调度环境)。本节将详细讲解 SynchronizationContext
的作用、await
的线程切换行为,以及 ConfigureAwait(false)
的使用。
1. SynchronizationContext 的作用
1.1 什么是 SynchronizationContext?
SynchronizationContext
是 .NET 中的一个抽象类,表示一个同步上下文。它在异步编程中用于协调一些操作的执行位置,例如:
- 在 UI 应用程序中,确保异步操作完成后回到主线程更新 UI。
- 在 ASP.NET 中,确保异步操作完成后代码继续在请求上下文中运行。
每种应用程序框架都会实现自己的 SynchronizationContext
:
- WinForms 和 WPF: 使用
WindowsFormsSynchronizationContext
确保异步操作完成后回到主线程操作 UI。 - ASP.NET: 为每个请求创建一个
AspNetSynchronizationContext
,确保异步操作完成后继续在该请求上下文中执行。 - ASP.NET Core: 默认没有
SynchronizationContext
,改用线程池执行异步操作。
1.2 如何在 UI 应用中切换到主线程?
在 UI 应用程序(如 WinForms 和 WPF)中,主线程负责管理用户界面。因此,当异步操作完成后,我们需要切换回主线程来更新 UI。这是通过 SynchronizationContext
实现的。
示例:WPF 应用中使用主线程更新 UI
private async void Button_Click(object sender, RoutedEventArgs e)
{
// 异步操作(默认捕获主线程 SynchronizationContext)
await Task.Delay(2000);
// 回到主线程,更新 UI
MyLabel.Content = "操作完成!";
}
在上面的代码中:
- 默认情况下,在
await
之后,代码通过捕获的SynchronizationContext
调度,最终在主线程中执行。 - 这使得我们可以安全地更新 UI,而无需显式调用
Dispatcher.Invoke
。
1.3 ASP.NET Core 默认不带 SynchronizationContext 的原因及优势
在 ASP.NET Core 中,默认的 SynchronizationContext
被移除,异步操作完成后直接在线程池中运行。这与传统的 ASP.NET 不同。
原因:
- 性能提升: 移除
SynchronizationContext
消除了请求上下文的绑定,减少了线程切换的开销。 - 无需线程绑定: ASP.NET Core 的设计目标是高性能和高并发,它允许异步操作在不同线程中完成,而不必回到特定线程或上下文。
优势:
- 异步操作的线程切换更少,提高了服务器的吞吐量。
- 开发者无需担心线程上下文绑定,代码可以更加自由地运行在线程池线程上。
示例:ASP.NET Core 中的异步方法
public async Task<IActionResult> GetDataAsync()
{
var data = await SomeAsyncOperation();
return Ok(data); // 无需返回到请求上下文
}
在 ASP.NET Core 中,await
后的代码会直接在线程池线程中运行,而不会尝试恢复到原始的请求上下文。
2. await 的线程切换行为
2.1 await 前后的线程是否一致?
await
的线程切换行为取决于是否捕获了 SynchronizationContext
或当前执行的线程上下文:
- 如果捕获了
SynchronizationContext
(例如在 WPF 或 WinForms 中),await
后的代码会切回到捕获的线程(通常是主线程)。 - 如果没有
SynchronizationContext
(例如在控制台应用程序或 ASP.NET Core 中),await
后的代码会继续在线程池中的线程运行。
示例:不同场景下 await
的线程切换
private async Task TestAsync()
{
Console.WriteLine($"Before await: Thread {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000); // 模拟异步操作
Console.WriteLine($"After await: Thread {Thread.CurrentThread.ManagedThreadId}");
}
运行结果:
-
在 WPF 或 WinForms 中:
Before await: Thread 1 After await: Thread 1
(因为捕获了主线程的
SynchronizationContext
) -
在控制台应用程序或 ASP.NET Core 中:
Before await: Thread 1 After await: Thread 4
(因为没有
SynchronizationContext
,await
后的代码可能运行在不同的线程上)
2.2 ConfigureAwait(false) 的适用场景及性能提升
ConfigureAwait
是一个重要的工具,用于控制 await
是否捕获当前的 SynchronizationContext
。
ConfigureAwait(true)
(默认): 捕获当前的SynchronizationContext
,await
后的代码会切换回原始上下文。ConfigureAwait(false)
: 不捕获SynchronizationContext
,await
后的代码在线程池中运行。
适用场景
-
ConfigureAwait(true)
:- 在需要返回到特定上下文的场景中使用,例如:
- WPF 或 WinForms 应用程序中更新 UI。
- ASP.NET 中恢复到请求上下文。
- 在需要返回到特定上下文的场景中使用,例如:
-
ConfigureAwait(false)
:- 在不依赖特定上下文的后台操作中使用,例如:
- 数据库查询、文件读写、网络请求等不涉及 UI 的操作。
- 提升性能,减少线程切换开销。
- 在不依赖特定上下文的后台操作中使用,例如:
性能提升
ConfigureAwait(false)
避免了捕获和恢复SynchronizationContext
的开销,尤其是在高并发场景下。- 在 ASP.NET Core 中,由于没有
SynchronizationContext
,默认行为类似于ConfigureAwait(false)
。
示例:使用 ConfigureAwait(false)
提升性能
public async Task ProcessDataAsync()
{
// 不捕获同步上下文,提高性能
var data = await GetDataFromDatabaseAsync().ConfigureAwait(false);
// 继续处理数据(运行在线程池线程上)
ProcessData(data);
}
14.7 async/await 与执行上下文的协作
在 .NET 中,执行上下文(ExecutionContext) 在异步代码和多线程编程中用于管理与逻辑操作相关的上下文信息。async/await
是 .NET 异步编程的核心特性,与执行上下文密切协作,以确保异步操作中的上下文一致性。本节将详细探讨执行上下文的原理、作用,以及它与 async/await
的协作方式。
1. 执行上下文(ExecutionContext)
1.1 什么是执行上下文
ExecutionContext
是 .NET 中的一个类,表示代码执行时的逻辑上下文。它封装了与当前线程和任务相关的一些信息,确保这些信息在异步方法或线程切换中能够正确传递。
执行上下文中包含的信息:
- 同步上下文(SynchronizationContext): 决定异步任务完成后代码的调度位置(如主线程)。
- 安全上下文(SecurityContext): 包含与线程安全相关的信息,例如用户身份验证和权限。
- 逻辑调用上下文(Logical Call Context): 支持跨线程传递的上下文数据,例如通过
CallContext
或AsyncLocal
存储的值。
执行上下文的核心作用是 在异步操作和线程切换中传递这些关键信息,以确保逻辑行为的一致性。
1.2 执行上下文的作用
执行上下文在以下方面具有重要作用:
-
上下文一致性:
- 在异步操作或线程切换中,执行上下文会捕获并恢复相关信息,使得上下文数据在整个异步调用链中保持一致。
- 例如,
HttpContext
和AsyncLocal
的值会通过执行上下文正确传递。
-
跨线程逻辑数据管理:
AsyncLocal
和CallContext
依赖执行上下文来在异步方法间传递数据。
-
安全性:
- 执行上下文可以携带用户身份、权限等信息,在异步方法中保证安全上下文的一致性。
-
调试和诊断:
- 执行上下文中的信息(例如请求 ID、日志上下文)可以用于跨线程/异步操作的调试和日志记录。
1.3 执行上下文的传递
执行上下文的传递是通过 捕获(Capture) 和 恢复(Restore) 实现的:
-
捕获:
- 当创建异步任务或启动新线程时,.NET 会捕获当前线程的执行上下文并与异步任务或线程关联。
- 捕获的内容包括同步上下文、AsyncLocal 值、权限信息等。
-
恢复:
- 当异步任务完成并切换回主上下文时,.NET 会恢复捕获的执行上下文,以确保上下文数据一致。
示例:执行上下文传递
AsyncLocal
会随着执行上下文进行传递,因此示例使用AsyncLocal
static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
static async Task Main(string[] args)
{
_asyncLocal.Value = "Main Context";
Console.WriteLine($"Before Task: {_asyncLocal.Value}");
await Task.Run(() =>
{
Console.WriteLine($"Inside Task Before: {_asyncLocal.Value}");
_asyncLocal.Value = "Task Context";
Console.WriteLine($"Inside Task After: {_asyncLocal.Value}");
});
Console.WriteLine($"After Task: {_asyncLocal.Value}");
}
输出结果:
Before Task: Main Context
Inside Task Before: Main Context
Inside Task After: Task Context
After Task: Main Context
分析:
- 在
Task.Run
中,执行上下文的值从主上下文传递给了异步任务。 - 异步任务修改了上下文值,但这种修改仅在异步任务范围内生效。
- 当返回到主上下文时,
_asyncLocal.Value
恢复为主上下文的值。
1.4 执行上下文与性能优化
捕获和恢复执行上下文是有开销的,尤其是在高并发或性能敏感的场景中。为了减少开销,可以采取以下优化措施:
-
禁止执行上下文的流动(SuppressFlow):
- 使用
ExecutionContext.SuppressFlow()
方法可以禁用执行上下文的捕获和恢复。 - 适用于不需要跨异步任务传递上下文数据的场景(例如纯计算任务)。
示例:禁用执行上下文流动
ExecutionContext.SuppressFlow(); await Task.Run(() => { // 这里不会捕获和传递主上下文的信息 Console.WriteLine(_asyncLocal.Value); // 输出为空 }); ExecutionContext.RestoreFlow();
- 使用
-
显式传递上下文数据:
- 如果上下文数据可以通过参数传递,则避免使用
AsyncLocal
或CallContext
。
- 如果上下文数据可以通过参数传递,则避免使用
-
限制上下文捕获:
- 在 ASP.NET Core 中,通过
ConfigureAwait(false)
可以避免捕获同步上下文,从而提升性能。
- 在 ASP.NET Core 中,通过
2. async/await 与执行上下文的协作
async/await
是如何做到执行上下文的一致性的呢?
2.1 await 前后如何保证执行上下文的一致性
在 async
方法中,await
会捕获当前线程的执行上下文ExecutionContext
,并在异步操作完成后恢复。
示例:async/await 的上下文捕获
static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
static async Task Main(string[] args)
{
_asyncLocal.Value = "Main Context";
Console.WriteLine($"Before await: {_asyncLocal.Value}");
await Task.Delay(100); // 异步操作,可能切换线程
Console.WriteLine($"After await: {_asyncLocal.Value}");
}
输出结果:
Before await: Main Context
After await: Main Context
分析:
- 在
await
之前,主上下文中的_asyncLocal.Value
被捕获。 - 异步任务完成后,捕获的上下文被恢复,因此上下文值保持一致。
原理:
前面我们已经知道,await后面的操作会被包装成异步任务回调执行,因此要确保await前后执行上下文一致性,就需要将执行上下文一起包装到回调中,这样回调执行时,就可以恢复其上下文。那是如何、何时将执行上下文传递到回调中的呢,看如下代码:
包装状态机:GetStateMachineBox
// 封装盒是一个特殊的数据结构,用于管理异步方法的执行状态
private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
ref TStateMachine stateMachine, // 状态机实例,用于跟踪异步方法的执行状态
[NotNull] ref Task<TResult>? taskField) // 可空的 Task<TResult> 引用,用于存储任务结果
where TStateMachine : IAsyncStateMachine // TStateMachine 必须实现 IAsyncStateMachine 接口
{
// 捕获当前的 ExecutionContext(执行上下文)
// 用于将当前线程的上下文(如同步上下文、文化信息等)传递给异步操作
ExecutionContext? currentContext = ExecutionContext.Capture();
IAsyncStateMachineBox result; // 定义返回值,封装当前的状态机实例
// 检查 taskField 是否已经是一个强类型的 AsyncStateMachineBox<TStateMachine>
if (taskField is AsyncStateMachineBox<TStateMachine> stronglyTypedBox)
{
// 如果封装盒的上下文与当前上下文不同,则更新上下文
if (stronglyTypedBox.Context != currentContext)
{
stronglyTypedBox.Context = currentContext;
}
// 将封装盒赋值给 result
result = stronglyTypedBox;
}
else
{
// 如果 taskField 为空或不是正确的封装盒,则创建一个新的封装盒
AsyncStateMachineBox<TStateMachine> box = new AsyncStateMachineBox<TStateMachine>();
// 将新的封装盒赋值给 taskField,这样外部代码可以访问到任务的状态
taskField = box;
// 初始化封装盒的状态机和上下文
box.StateMachine = stateMachine;
box.Context = currentContext;
// 将封装盒赋值给 result
result = box;
}
// 返回封装盒
return result;
}
解读:
其实也就是在包装回调状态机的时候将执行上下文一起打包了。然后异步任务完成后执行回调时,判断有没有特定的执行上下文,有就恢复。
2.2 线程如何确保调用异步任务后的执行上下文不被改变?
前面我们已经知道,启动一个异步操作,实际就是启动这个异步操作对应的状态机,执行其MoveNext
方法。
启动代码如下:
/// <summary>
/// 启动状态机的执行。
/// </summary>
/// <typeparam name="TStateMachine">状态机的类型。</typeparam>
/// <param name="stateMachine">状态机实例,按引用传递。</param>
[DebuggerStepThrough]
public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
if (stateMachine == null) // 确保状态机实例不为 null
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
}
// 获取当前线程的执行上下文和同步上下文
Thread currentThread = Thread.CurrentThread;
ExecutionContext? previousExecutionCtx = currentThread._executionContext;
SynchronizationContext? previousSyncCtx = currentThread._synchronizationContext;
try
{
stateMachine.MoveNext(); // 启动状态机
}
finally
{
// 如果同步上下文发生了变化,恢复为之前的同步上下文
if (previousSyncCtx != currentThread._synchronizationContext)
{
currentThread._synchronizationContext = previousSyncCtx;
}
// 如果执行上下文发生了变化,恢复为之前的执行上下文
if (previousExecutionCtx != currentThread._executionContext)
{
ExecutionContext.RestoreChangedContextToThread(currentThread, previousExecutionCtx, currentThread._executionContext);
}
}
}
解读:
- 可以看到,Start方法在启动异步操作前会先获取当前的执行上下文
ExecutionContext
,然后在启动异步操作后恢复执行上下文ExecutionContext
- 同时,同步上下文也会被捕获,启动状态机后被恢复。
14.8 AsyncLocal 与异步编程中的数据流转
- AsyncLocal 的定义:
- 如何实现异步任务中的上下文数据流转?
- 与 ThreadLocal 的对比。
- 性能与最佳实践:
- AsyncLocal 的开销是否可控?
- 适用于哪些场景?
14.9 async/await 与 Task 的常见误区
async/await
和 Task
是 .NET 中异步编程的核心工具,但它们的工作机制容易被误解,导致许多开发者在编写异步代码时犯下常见错误。本节将澄清一些常见的误区,帮助开发者更好地理解和使用这些工具。
1. async 不等于多线程
1.1 为什么异步方法并不一定会创建线程?
一个常见的误解是,async
方法会自动创建新的线程。但实际上,async
和线程没有直接关系。async/await
关注的是 非阻塞,而不是并行计算。
- 异步方法的主要目的是释放当前线程,在等待操作完成时让线程可以执行其他任务,而不是专门创建新线程。
- 异步操作(例如网络 I/O、文件 I/O)通常由操作系统或硬件处理,不需要线程的参与。只有当异步操作完成时,任务调度器才会安排一个线程来继续执行后续代码。
示例:异步方法不创建线程
static async Task Main(string[] args)
{
Console.WriteLine($"Start: Thread {Thread.CurrentThread.ManagedThreadId}");
// 异步等待 I/O 操作,线程未阻塞
await Task.Delay(1000);
Console.WriteLine($"End: Thread {Thread.CurrentThread.ManagedThreadId}");
}
输出:
Start: Thread 1
End: Thread 1
分析:
Task.Delay
模拟异步 I/O 操作,不占用任何线程。- 异步任务完成后,代码继续在原线程上执行。
1.2 async/await 关注的是非阻塞,而不是并行
async/await
的设计目标是优化程序的可伸缩性,而非提高并行度。通过释放线程资源,async/await
可以让线程池中的线程处理更多任务,从而提升系统吞吐量。
如果需要真正的并行计算(例如 CPU 密集型任务),可以使用 Task.Run
将任务分配到线程池线程,但这不是 async/await
的核心功能。
误区:async 方法自动并行执行
async Task<int> ComputeAsync()
{
// 不会自动并行,每个方法仍然是顺序执行
return await Task.FromResult(42);
}
async Task MainAsync()
{
var result1 = await ComputeAsync();
var result2 = await ComputeAsync();
Console.WriteLine(result1 + result2); // 顺序执行
}
正确理解:
- 异步方法的执行顺序与普通方法相同,
async
只是通过await
暂停方法的执行,让线程可以去处理其他任务。
2. await 的行为
2.1 await 会阻塞线程吗?
await
不会阻塞线程。它的作用是“暂时挂起”代码的执行,释放当前线程以便执行其他任务。当异步操作完成后,await
会通过回调将挂起的代码恢复执行。
示例:await 不阻塞线程
static async Task Main(string[] args)
{
Console.WriteLine("Before await");
// 模拟异步 I/O 操作
await Task.Delay(1000);
Console.WriteLine("After await");
}
输出:
Before await
After await
分析:
- 在
Task.Delay
的等待期间,当前线程被释放,主线程没有被阻塞。
2.2 await 后的代码总是在当前线程上运行吗?
await
后的代码是否运行在当前线程上,取决于是否有 同步上下文(SynchronizationContext)。
- 在 WPF 或 WinForms 等 UI 应用中,
SynchronizationContext
会强制await
后的代码返回到 UI 线程。 - 在 ASP.NET Core 或控制台应用中,默认没有
SynchronizationContext
,await
后的代码可能运行在不同的线程上。
示例:await 后的线程可能不同
static async Task Main(string[] args)
{
Console.WriteLine($"Thread before await: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000);
Console.WriteLine($"Thread after await: {Thread.CurrentThread.ManagedThreadId}");
}
输出(控制台应用中):
Thread before await: 1
Thread after await: 4
分析:
- 在控制台应用中,
await
后的代码可能运行在不同的线程上(通常是线程池线程)。 - 如果需要优化性能,可以通过
ConfigureAwait(false)
避免捕获上下文。
3. Task.Run 的错误使用
3.1 为什么不应该滥用 Task.Run?
Task.Run
是一个用于将任务分配到线程池线程的方法,但滥用它会导致以下问题:
-
不必要的线程切换:
- 如果任务本身是异步的(如 I/O 操作),不需要额外使用线程池线程。
-
线程池资源浪费:
- 线程池的线程资源有限,滥用
Task.Run
会导致线程池饱和,影响其他任务的执行。
- 线程池的线程资源有限,滥用
错误示例:滥用 Task.Run 包装异步方法
// 错误:Task.Run 包装异步方法,额外增加线程切换
await Task.Run(async () =>
{
await Task.Delay(1000); // 异步操作不需要线程池线程
});
正确做法:直接调用异步方法
// 正确:直接使用异步方法,无需额外创建线程
await Task.Delay(1000);
3.2 CPU 密集型任务与 I/O 密集型任务的正确处理方式
-
CPU 密集型任务:
- 使用
Task.Run
将任务分配到线程池线程,避免阻塞主线程。 - 示例:图像处理、加密运算等。
// 使用 Task.Run 处理 CPU 密集型任务 var result = await Task.Run(() => { return Compute(); });
- 使用
-
I/O 密集型任务:
- 直接使用异步方法(如
await
文件 I/O 或网络请求),不需要Task.Run
。 - 示例:读取文件、调用网络 API。
// 使用 I/O 异步方法 var data = await File.ReadAllTextAsync("data.txt");
- 直接使用异步方法(如
4. 并发与并行
4.1 async/await 是否等于并发?
async/await
本身并不代表并发。await
会暂停方法的执行,释放线程资源,但代码仍然是顺序执行的。
- 如果需要并发执行多个任务,需要显式启动多个任务(例如使用
Task.WhenAll
或Task.WhenAny
)。
4.2 如何通过 Task.WhenAll 实现真正的并发?
通过 Task.WhenAll
可以同时启动多个任务并等待它们全部完成,以实现真正的并发。
示例:并发执行多个任务
async Task MainAsync()
{
// 启动多个任务
var task1 = Task.Delay(1000);
var task2 = Task.Delay(2000);
var task3 = Task.Delay(3000);
// 等待所有任务完成
await Task.WhenAll(task1, task2, task3);
Console.WriteLine("All tasks completed");
}
输出:
任务并发执行,总耗时约为 3 秒。
14.10 async/await 的性能优化与高级用法
async/await
提供了一种简单高效的异步编程模型,但在高性能应用中,默认的 Task
和状态机生成可能会引入额外的开销。本节从性能优化和高级用法两个方面,探讨如何进一步提升 async/await
的性能,以及如何定制异步行为。
1. 性能优化
1.1 使用 ValueTask
优化高频异步方法的性能
在某些高频调用的场景中,返回 Task
会引入额外的内存分配,因为每次调用都会创建一个新的 Task
对象。对于快速完成的异步方法,这种内存分配是没有必要的。在这些场景下,可以使用 ValueTask
来优化性能。
- 什么是
ValueTask
?
ValueTask
是 .NET 提供的一种轻量级异步返回类型,用于避免不必要的任务分配。如果一个异步方法经常同步完成,ValueTask
可以直接返回结果,而无需创建额外的Task
对象。
示例:使用 ValueTask 优化简单异步方法
参考 第三章
1.2 避免不必要的上下文捕获 (ConfigureAwait(false)
的使用场景)
await
默认会捕获当前的上下文(SynchronizationContext
或 TaskScheduler
),并在异步操作完成后恢复到该上下文。这种上下文捕获在大多数场景下是必要的(如 UI 应用程序),但在后台任务或性能敏感的场景中,这可能会增加不必要的开销。
-
性能问题:
上下文捕获和恢复会导致额外的线程切换,这在高并发场景中是昂贵的。 -
解决方案:
使用ConfigureAwait(false)
避免捕获上下文,从而减少开销。
示例:使用 ConfigureAwait(false) 优化性能
async Task PerformBackgroundTaskAsync()
{
// 模拟异步操作
await Task.Delay(100).ConfigureAwait(false);
// 此处代码不需要回到原上下文
Console.WriteLine("Task completed on thread pool");
}
使用场景:
- 在后台服务(如 ASP.NET Core)中,异步方法通常不需要捕获上下文。
- 在性能敏感的场景中,尽量避免不必要的上下文切换。
1.3 减少状态机的开销(async 方法何时不生成状态机?)
async
方法会被编译器转换成状态机,以支持挂起和恢复操作。但如果方法不包含 await
或可以同步完成,编译器会优化,避免生成状态机。
示例:不生成状态机的场景(正确示范)
// 不包含 await,不会生成状态机
Task<int> GetValueAsync()
{
return Task.FromResult(42); // 直接返回结果
}
或者
// 不包含 await,不会生成状态机
Task<int> GetValueAsync()
{
return Task.Run(...); // 直接返回Task
}
示例:生成状态机的场景(不建议示范)
// 不包含 await,不会生成状态机
async Task<int> GetValueAsync()
{
return await Task.FromResult(42); // 直接返回结果
}
或者
// 不包含 await,不会生成状态机
async Task<int> GetValueAsync()
{
return await Task.Run(...); // 直接返回Task
}
注意:
- 如果方法包含
await
,状态机是不可避免的。 - 对于简单的同步返回场景,避免使用
async
修饰符是更高效的选择。
2. 高级用法
2.1 如何为自定义类型添加 await 支持?
2.3 使用 TaskCompletionSource
手动控制异步任务
TaskCompletionSource
是一个强大的工具,用于手动控制异步任务的完成状态。它通常用于桥接同步代码和异步代码之间的调用。
- 常见场景:
- 封装回调形式的异步操作。
- 提供更精细的异步任务控制。
示例:使用 TaskCompletionSource 封装异步回调
public static Task<int> GetResultAsync()
{
var tcs = new TaskCompletionSource<int>();
// 模拟异步回调
Task.Run(() =>
{
Thread.Sleep(1000); // 模拟耗时操作
tcs.SetResult(42); // 手动设置任务结果
});
return tcs.Task;
}
static async Task Main(string[] args)
{
int result = await GetResultAsync();
Console.WriteLine($"Result: {result}");
}
输出:
Result: 42
注意:
TaskCompletionSource
提供了SetResult
、SetException
和SetCanceled
方法,用于手动控制任务状态。- 使用时需要注意线程安全。