await 后面接的函数一定是 返回Task的函数对吗
1.await 后面接的函数一定是 返回Task的函数对吗
不完全是。
await 后面接的表达式,不一定非要返回 Task 或 Task<T>。
准确地说,await 后面接的对象必须满足 “可等待模式” (Awaitable Pattern)。虽然 Task 和 Task<T> 是最常见、最标准的实现,但 C# 允许任何类型只要具备特定的成员结构,就可以被 await。
1. 核心规则:什么是“可等待模式”?
编译器在编译 await x 时,会检查 x 的类型是否满足以下条件(按优先级):
- 拥有
GetAwaiter()方法:该方法返回一个“等待者”对象。 - 等待者对象必须实现接口:
INotifyCompletion(基础)- 或者
ICriticalNotifyCompletion(高级)
- 等待者对象必须包含以下成员:
bool IsCompleted { get; }:判断是否已完成。void OnCompleted(Action continuation)(或UnsafeOnCompleted):注册回调,告诉程序完成后该做什么。void GetResult()(或T GetResult()):获取结果或抛出异常。
只要满足上述结构,无论它是不是 Task,都可以被 await。
2. 常见的非 Task 可等待对象举例
A. ValueTask / ValueTask<T> (高性能场景)
这是 .NET Core 2.0+ 引入的,用于减少内存分配。
- 区别:
Task是类 (Reference Type),每次创建都在堆上分配内存;ValueTask是结构体 (Value Type),如果操作同步完成,它甚至不需要堆分配。 - 用法:完全可以直接
await。
public async ValueTask<int> GetValueAsync()
{
// 模拟可能同步完成的逻辑
return await new ValueTask<int>(42);
}
B. 自定义的可等待类型 (高级玩法)
你可以自己写一个类,让它能被 await。这通常用于构建特殊的异步原语(如自定义的锁、信号量、或者像 UniTask 这样的游戏开发库)。
示例:一个简单的“秒针”等待器
这个例子演示如何创建一个不是 Task 但可以被 await 的类。
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
// 1. 定义等待者 (Awaiter)
public class SecondHandAwaiter : INotifyCompletion
{
private readonly int _seconds;
private Action _continuation;
public SecondHandAwaiter(int seconds)
{
_seconds = seconds;
}
// 判断是否完成 (这里简单模拟,实际可能检查时间)
public bool IsCompleted => false;
// 注册回调:当任务完成时,执行这个 action
public void OnCompleted(Action continuation)
{
_continuation = continuation;
// 启动一个后台线程或 Task 来模拟等待,完成后调用回调
Task.Run(async () =>
{
await Task.Delay(_seconds * 1000);
_continuation?.Invoke(); // 通知主流程继续
});
}
// 获取结果 (这里是 void,所以返回空)
public void GetResult() { }
}
// 2. 定义可等待对象 (Awaitable)
public class WaitSeconds
{
private readonly int _seconds;
public WaitSeconds(int seconds) => _seconds = seconds;
// 关键:提供 GetAwaiter 方法
public SecondHandAwaiter GetAwaiter()
{
return new SecondHandAwaiter(_seconds);
}
}
// 3. 使用示例
class Program
{
static async Task Main()
{
Console.WriteLine($"开始等待: {DateTime.Now:ss}");
// 【关键点】这里 await 的不是 Task,而是我们自定义的 WaitSeconds 类!
await new WaitSeconds(2);
Console.WriteLine($"等待结束: {DateTime.Now:ss}");
}
}
C. WinRT (UWP) 中的 IAsyncAction / IAsyncOperation
在开发 UWP (Universal Windows Platform) 应用时,Windows 运行时 API 返回的是 IAsyncAction 等接口,而不是 Task。
- C# 编译器内置了对这些接口的支持,可以直接
await它们,无需手动转换。
3. 特殊情况:ConfigureAwait 的返回值
有时候你看到的代码是这样的:
await someTask.ConfigureAwait(false);
someTask返回的是Task。- 但
someTask.ConfigureAwait(false)返回的是一个ConfiguredTaskAwaitable结构体。 - 你
await的实际上是这个结构体,而不是原始的Task。这也是利用了“可等待模式”。
总结
| 问题 | 答案 |
|---|---|
await 后面必须是 Task 吗? |
不是。 |
| 那必须是什么? | 必须是实现了 Awaitable Pattern 的对象(即有 GetAwaiter() 且返回的 Awaiter 符合规范)。 |
| 日常开发中我会遇到非 Task 的情况吗? | 偶尔会。最常见的是 ValueTask (为了性能) 或 ConfigureAwait 的返回值。自定义可等待对象通常只在编写底层库时使用。 |
| 最佳实践建议 | 在编写自己的异步 API 时,默认始终返回 Task 或 Task<T>。只有在极高性能敏感场景(如高频循环、游戏帧更新)下,才考虑返回 ValueTask。不要为了炫技去自定义可等待类型,除非你非常清楚自己在做什么。 |
所以,虽然 Task 是绝对的主流,但 await 的机制设计得比 Task 更通用、更灵活。
2.Awaitable Pattern 到Task的继承关系
这是一个非常关键的概念澄清:
Task 并没有“继承” Awaitable Pattern,反过来也不成立。
它们之间的关系是 “实现者 (Implementor)” 与 “协议/模式 (Pattern)” 的关系,而不是父子继承关系。
1. 核心关系图解
-
Awaitable Pattern (可等待模式):
- 它不是一个类,也不是一个接口。
- 它是 C# 编译器识别的一种代码结构规范(鸭子类型)。
- 它没有继承树,因为它根本不存在于运行时类型系统中。
-
Task/Task<T>:- 它们是 .NET Framework/Core 中的具体类 (
public sealed class Task)。 - 它们内部实现了符合 Awaitable Pattern 所需的所有方法(
GetAwaiter,IsCompleted等)。 - 因此,
Task是 Awaitable Pattern 的最著名、最标准的实现者之一。
- 它们是 .NET Framework/Core 中的具体类 (
比喻:
- Awaitable Pattern 就像是 “会飞的能力”(一种标准:只要有翅膀、能扇动、能产生升力)。
Task就像是 “波音 747 飞机”。- 关系:波音 747 拥有 会飞的能力(实现了模式),但波音 747 并不是“继承”自“会飞的能力”。
- 其他实现者:鸟、
ValueTask、自定义的WaitSeconds类,它们也都“拥有”会飞的能力,但它们和波音 747 之间没有直接的继承关系。
2. 为什么看起来像继承?
因为 Task 类内部确实写了符合模式要求的代码。让我们看看 Task 是如何“伪装”成符合模式的:
在 Task 类的源码中(简化版),它定义了这样一个方法:
public sealed class Task : IAsyncResult, IDisposable
{
// ... 其他成员 ...
// 这就是让 Task 符合 Awaitable Pattern 的关键方法
public TaskAwaiter GetAwaiter()
{
return new TaskAwaiter(this);
}
}
而返回的 TaskAwaiter 结构体则严格遵循了模式规范:
public struct TaskAwaiter : INotifyCompletion, ICriticalNotifyCompletion
{
// 1. 实现接口
// 2. 属性
public bool IsCompleted { get { ... } }
// 3. 方法
public void OnCompleted(Action continuation) { ... }
public void UnsafeOnCompleted(Action continuation) { ... }
public void GetResult() { ... }
}
结论:Task 通过包含这些特定的成员,从而满足了编译器的检查条件。这是一种组合或内部实现,而非继承。
3. 如果强行画继承图(逻辑上的)
如果在逻辑上非要表示这种关系,它不是类继承图,而是实现关系图:
graph TD
A[await 关键字] -->|编译时检查 | B(Awaitable Pattern 规范)
B -.->|要求具备 | C[GetAwaiter() 方法]
B -.->|要求返回对象具备 | D[IsCompleted, OnCompleted, GetResult]
E[Task 类] -->|内部实现了 | C
E -->|返回 | F[TaskAwaiter 结构体]
F -->|内部实现了 | D
G[ValueTask 结构体] -->|内部实现了 | C
G -->|返回 | H[ValueTaskAwaiter 结构体]
H -->|内部实现了 | D
I[自定义类 MyClass] -->|内部实现了 | C
I -->|返回 | J[MyAwaiter 结构体]
J -->|内部实现了 | D
style B fill:#f9f,stroke:#333,stroke-dasharray: 5 5
style E fill:#bbf,stroke:#333
style G fill:#bbf,stroke:#333
style I fill:#bbf,stroke:#333
- 虚线框 (Pattern):不是实体,只是规则。
- 实线框 (Classes/Structs):
Task,ValueTask,MyClass都是独立的类型,它们互不继承,但都独立地“实现”了规则。
4. 常见误区澄清
误区 1:“Task 继承了 INotifyCompletion?”
错。
Task类本身没有实现INotifyCompletion。- 是
Task返回的那个TaskAwaiter结构体 实现了INotifyCompletion。 Task只是提供了一个工厂方法GetAwaiter()来生产这个结构体。
误区 2:“我想让我的类继承 Task,这样它就能被 await 了?”
错且不可能。
Task是sealed的,禁止继承。- 即使能继承,继承
Task也不会自动让你支持await(虽然Task本身支持,但你无法扩展其内部逻辑)。 - 正确做法是:让你的类实现
GetAwaiter()方法,返回一个符合规范的 Awaiter(如前文所述的DelayAwaiter)。
误区 3:“所有能被 await 的对象都是 Task 的子类?”
错。
ValueTask是结构体,不能继承类。- 自定义的
WaitSeconds是普通类,与Task毫无血缘关系。 - 它们唯一的共同点是都符合 Awaitable Pattern。
总结
- 关系定义:
Task是 Awaitable Pattern 的一等公民实现者。 - 技术本质:这是基于命名约定和方法签名的编译器特性(鸭子类型),而非面向对象的多态继承。
- 开发启示:你不需要(也不能)通过继承
Task来获得await能力。你只需要在你的类中照猫画虎,写上GetAwaiter()并返回一个符合规范的辅助对象即可。
3.Task 简单应用举例
这里提供 4 个最实用、最简单的 Task 应用举例,涵盖了界面不卡顿、并发加速、超时控制和后台定时任务场景。
1. 场景一:保持界面/主线程流畅 (最常见)
问题:如果在主线程(如 WinForms/WPF 按钮点击事件或 ASP.NET 请求)中直接调用耗时操作(如下载文件、查数据库),界面会“假死”或请求阻塞。
解决:使用 async/await 将耗时操作异步化。
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class SimpleDownload
{
public static async Task StartDownloadAsync()
{
Console.WriteLine("1. 开始下载 (界面未卡死)...");
using (HttpClient client = new HttpClient())
{
// 【关键点】await 让出当前线程,去干别的事,直到下载完成才回来继续执行
// 这里的 Task.Delay 模拟网络延迟,实际开发中替换为 client.GetStringAsync(...)
await Task.Delay(2000);
string result = "数据下载完成!";
Console.WriteLine($"2. {result}");
}
Console.WriteLine("3. 下载结束,可以继续响应用户操作。");
}
}
// 调用方式 (例如在按钮点击事件中):
// await SimpleDownload.StartDownloadAsync();
2. 场景二:并发执行以加速 (批量处理)
问题:你有 3 个独立的任务(比如请求 3 个不同的 API),如果串行执行需要 3 秒。
解决:同时启动它们,使用 Task.WhenAll 等待全部完成,总耗时仅需最慢的那个任务的时间(约 1 秒)。
using System;
using System.Threading.Tasks;
public class BatchProcess
{
public static async Task RunParallelAsync()
{
Console.WriteLine("开始并行处理...");
var startTime = DateTime.Now;
// 【关键点】同时创建并启动三个任务,而不是依次等待
Task<int> task1 = DoWorkAsync("任务A", 1000);
Task<int> task2 = DoWorkAsync("任务B", 1200);
Task<int> task3 = DoWorkAsync("任务C", 800);
// 【关键点】等待所有任务完成
int[] results = await Task.WhenAll(task1, task2, task3);
var endTime = DateTime.Now;
Console.WriteLine($"全部完成!总耗时: {(endTime - startTime).TotalMilliseconds}ms");
Console.WriteLine($"结果集合: [{string.Join(", ", results)}]");
}
// 模拟耗时工作
private static async Task<int> DoWorkAsync(string name, int delayMs)
{
await Task.Delay(delayMs);
Console.WriteLine($"{name} 完成");
return delayMs; // 返回耗时作为结果
}
}
3. 场景三:简单的超时控制
问题:某个操作可能卡死,你希望如果超过 2 秒没反应就自动取消并报错。
解决:利用 Task.WhenAny 对比“业务任务”和“延时任务”。
using System;
using System.Threading.Tasks;
public class TimeoutExample
{
public static async Task RunWithTimeoutAsync()
{
Console.WriteLine("开始执行可能超时的任务...");
// 模拟一个需要 5 秒才能完成的慢任务
Task slowTask = Task.Delay(5000).ContinueWith(t => "成功结果");
// 创建一个 2 秒后完成的超时任务
Task timeoutTask = Task.Delay(2000);
// 【关键点】谁先完成就返回谁
var finishedTask = await Task.WhenAny(slowTask, timeoutTask);
if (finishedTask == timeoutTask)
{
Console.WriteLine("❌ 错误:操作超时!(超过2秒)");
// 这里可以选择抛出异常或返回默认值
return;
}
// 如果是慢任务先完成,获取结果
string result = await slowTask;
Console.WriteLine($"✅ 成功:{result}");
}
}
4. 场景四:后台定时循环 (非阻塞版)
问题:需要一个后台任务每隔一段时间执行一次(如心跳检测、日志上传),但不能用 while(true) 死循环阻塞线程。
解决:在 async 方法中使用 await Task.Delay 进行循环。
using System;
using System.Threading;
using System.Threading.Tasks;
public class BackgroundWorker
{
// 用于控制停止
private static CancellationTokenSource _cts;
public static async Task StartLoopAsync()
{
_cts = new CancellationTokenSource();
var token = _cts.Token;
Console.WriteLine("后台任务已启动...");
try
{
while (!token.IsCancellationRequested)
{
// 执行工作
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 执行心跳检查...");
// 【关键点】非阻塞等待 3 秒。如果不加 await,循环会瞬间跑完;如果加 Thread.Sleep,会阻塞线程。
await Task.Delay(3000, token);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("后台任务已正常停止。");
}
}
public static void StopLoop()
{
_cts?.Cancel();
}
}
总结:什么时候用哪个?
| 场景 | 核心代码模式 | 目的 |
|---|---|---|
| 不想卡住界面/请求 | await SomeAsyncMethod() |
提升用户体验,释放线程资源 |
| 多个独立任务一起跑 | await Task.WhenAll(t1, t2...) |
缩短总耗时 (并发) |
| 防止程序无限等待 | await Task.WhenAny(task, Task.Delay(time)) |
增加系统的健壮性 |
| 周期性后台工作 | while(!cancel) { ... await Task.Delay(...) } |
实现非阻塞的轮询 |
你可以直接复制这些代码片段到控制台项目 (Program.cs) 的 Main 方法中测试(记得把 Main 改为 static async Task Main)。
4.Task的本质是什么,是线程吗
结论先行:Task 本质上不是线程。
这是一个非常常见的误解。简单来说:
- 线程 (Thread) 是操作系统层面的执行单元(工人)。
- Task 是 .NET 层面的异步操作承诺/对象(工单)。
核心区别:
- 一对多关系:一个
Task可能不需要任何线程(如已完成的计算),也可能在运行过程中切换多个线程,或者多个Task共享同一个线程。 - 轻量级:创建线程开销很大(需要分配栈内存、内核上下文切换),而创建
Task开销极小(只是一个堆上的对象)。 - 抽象层:
Task是“做什么”(What),线程是“怎么做”(How)。Task默认由 线程池 (ThreadPool) 来调度执行,但它也可以由其他机制调度(如 IO 完成端口)。
深入解析:Task 的三种存在形态
为了理解 Task 的本质,我们需要看它在不同场景下是如何工作的:
1. 真正的异步 IO (不占用线程)
这是 Task 最强大的地方。当你执行 await File.ReadAllTextAsync() 或 await HttpClient.GetAsync() 时:
- 发生了什么:操作系统发起一个 IO 请求(如读取磁盘或网络包)。
- 线程状态:没有线程在等待这个结果。当前线程立即返回去处理其他工作(如渲染 UI 或响应其他请求)。
- 完成时:当硬件(网卡/磁盘)完成工作后,通过中断通知 CPU,.NET 运行时才会从线程池里临时借一个线程来继续执行
await之后的代码。 - 本质:这里的
Task只是代表“未来某个时刻会完成的一个事件”,期间零线程消耗。
2. CPU 密集型任务 (占用线程池线程)
当你使用 Task.Run(() => { ... }) 时:
- 发生了什么:你将一段代码包装成
Task,并提交给 线程池 (ThreadPool)。 - 线程状态:线程池管理器会找一个空闲的线程(或创建一个新线程)来执行这段代码。
- 本质:这里的
Task是一个工作项 (Work Item)。它本身不是线程,而是排队等待被线程执行的任务。- 类比:
Task是餐厅里的“点菜单”,线程是“厨师”。点菜单本身不是厨师,它只是告诉厨师该做什么。
- 类比:
3. 已完成的任务 (无线程)
当你使用 Task.FromResult(42) 时:
- 发生了什么:结果已经算好了,直接返回一个标记为“已完成”的
Task对象。 - 线程状态:不需要任何线程参与调度。
- 本质:它只是一个包含结果数据的状态对象。
直观对比表
| 特性 | 线程 (Thread) | Task |
|---|---|---|
| 层级 | 操作系统内核级 (Kernel Level) | .NET 应用级 (User Level) |
| 资源消耗 | 高 (默认 1MB 栈空间,上下文切换昂贵) | 低 (仅几十到几百字节的对象) |
| 数量限制 | 有限 (通常几百到几千,多了系统崩溃) | 几乎无限 (可创建百万级) |
| 生命周期 | 创建 -> 运行 -> 销毁 | 创建 -> 等待/运行 -> 完成 (对象由 GC 回收) |
| 主要用途 | 并行执行代码 | 表示异步操作、组合操作、超时控制 |
| 调度者 | 操作系统调度器 | .NET TaskScheduler (默认基于线程池) |
代码验证:Task 如何“欺骗”线程
下面的代码可以证明 Task 和 Thread 不是一一对应的。
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine($"主线程 ID: {Thread.CurrentThread.ManagedThreadId}");
// 启动 5 个 Task
var tasks = new Task[5];
for (int i = 0; i < 5; i++)
{
int index = i;
tasks[i] = Task.Run(async () =>
{
// 1. 刚开始执行时的线程
int startThread = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"任务 {index} 开始于线程: {startThread}");
// 2. 模拟异步 IO (释放当前线程!)
// 在这 100ms 内,没有任何线程在等待这个任务
await Task.Delay(100);
// 3. 恢复执行时的线程 (很可能变了!)
int endThread = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"任务 {index} 恢复于线程: {endThread} (相同?{startThread == endThread})");
});
}
await Task.WhenAll(tasks);
Console.WriteLine("所有任务完成。");
Console.WriteLine($"最后回到主线程 ID: {Thread.CurrentThread.ManagedThreadId}");
}
}
运行结果预测:
- 你会看到 5 个任务可能只使用了 1 到 2 个线程池线程轮流执行(因为
Delay释放了线程)。 - 同一个任务在
await前后,线程 ID 经常会发生变化。- 如果
Task就是线程,线程 ID 绝不可能变。 - 正因为
Task只是“逻辑流”,它可以在不同的物理线程上“断点续传”。
- 如果
总结
- 不要说:“我创建了一个 Task 线程。” (错误)
- 要说:“我创建了一个 Task,它可能会在线程池的某个线程上运行,或者在 IO 完成时由系统唤醒。” (正确)
一句话比喻:
如果把线程比作出租车司机,那么 Task 就是乘客的订单。
- 一个订单(Task)可以由司机 A 接一半,堵车时放下,再由司机 B 接着开(异步上下文切换)。
- 如果是机场接送(IO 操作),订单生成后,司机可以去接别的客,等飞机到了(IO 完成),系统再指派任意一个空闲司机去执行后续行程。
- 订单本身不是司机,订单只是描述了“需要从哪去哪”以及“完成了没”。
5.Awaitable Pattern 的对象
Awaitable Pattern(可等待模式) 是 C# 编译器的一种“鸭子类型”(Duck Typing)机制。
它的核心思想是:编译器不关心你的对象是什么类型(是不是 Task,是不是继承自某个基类),只关心它是否长成了“特定的样子”。只要你的对象具备特定的方法签名,编译器就允许你在它前面使用 await 关键字。
要实现这个模式,你需要构建两个部分:
- Awaitable 对象:被
await的对象本身。 - Awaiter 对象:由 Awaitable 对象产生的“等待者”,负责真正处理异步逻辑。
1. 必须满足的结构规范
A. Awaitable 对象(外壳)
这个对象只需要包含一个方法:
public MyAwaiter GetAwaiter() { ... }
- 返回类型
MyAwaiter可以是任何类型(类、结构体)。 - 方法名必须是
GetAwaiter。 - 可以是实例方法或扩展方法。
B. Awaiter 对象(核心引擎)
这是真正干活的对象,它必须满足以下三个硬性条件:
-
实现接口:
- 必须实现
System.Runtime.CompilerServices.INotifyCompletion - (可选但推荐)实现
ICriticalNotifyCompletion(用于更底层的控制)
- 必须实现
-
具备属性:
public bool IsCompleted { get; }- 返回
true表示操作已经完成(此时await会同步继续执行,不挂起)。 - 返回
false表示操作未完成(此时await会挂起当前方法,注册回调)。
- 返回
-
具备方法:
-
注册回调:
public void OnCompleted(Action continuation) { ... } // 或者如果实现了 ICriticalNotifyCompletion: // public void UnsafeOnCompleted(Action continuation) { ... }- 当
IsCompleted为 false 时,编译器调用此方法。 - 你需要把
continuation(即await之后的代码)保存起来,等到异步操作完成后手动调用它。
- 当
-
获取结果:
public void GetResult() { ... } // 如果有返回值,则是 public T GetResult() { ... }- 当操作完成,恢复执行时调用。
- 如果操作失败,这里应该抛出异常。
- 如果操作成功,这里返回结果(或什么都不做如果是 void)。
-
2. 完整代码实现示例
下面是一个从零开始手写的 Awaitable Pattern 示例。我们要创建一个 WaitForSeconds 类,让你能写 await new WaitForSeconds(2),而不依赖 Task.Delay。
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
// ==========================================
// 1. 定义 Awaiter (等待者)
// 必须实现 INotifyCompletion
// ==========================================
public class DelayAwaiter : INotifyCompletion
{
private readonly int _milliseconds;
private Action _continuation; // 用来存储 "await 之后要执行的代码"
public DelayAwaiter(int milliseconds)
{
_milliseconds = milliseconds;
}
// 【关键属性】告诉编译器现在是否已经完成
// 这里我们简单起见,总是返回 false,强制走异步流程
// 实际场景中,如果时间 <= 0,这里可以返回 true
public bool IsCompleted => false;
// 【关键方法】注册回调
// 当任务未完成时,编译器会把 "剩余代码" 包装成 Action 传进来
public void OnCompleted(Action continuation)
{
_continuation = continuation;
// --- 模拟异步操作开始 ---
// 在实际生产中,这里可能是注册事件、发起 IO 请求等
// 这里我们用 ThreadPool 模拟一个定时器
ThreadPool.QueueUserWorkItem(_ =>
{
Thread.Sleep(_milliseconds); // 模拟耗时
// --- 异步操作结束 ---
// 操作完成后,手动触发之前注册的回调,让程序继续往下跑
// 注意:通常需要在上下文中恢复执行,这里简化直接调用
_continuation?.Invoke();
});
}
// 【关键方法】获取结果
// 如果 await 表达式有返回值 (如 var x = await obj),这里返回 T
// 如果没有返回值 (如 await obj),这里返回 void
// 如果出错了,在这里抛出异常
public void GetResult()
{
// 这里没有结果返回,也没有错误,直接通过
// 如果有错误,可以 throw new Exception("...");
}
}
// ==========================================
// 2. 定义 Awaitable (可等待对象)
// ==========================================
public class WaitForSeconds
{
private readonly int _ms;
public WaitForSeconds(int ms)
{
_ms = ms;
}
// 【核心入口】编译器看到 await 时,首先找这个方法
public DelayAwaiter GetAwaiter()
{
return new DelayAwaiter(_ms);
}
}
// ==========================================
// 3. 使用测试
// ==========================================
class Program
{
static async Task Main()
{
Console.WriteLine($"[主线程:{Thread.CurrentThread.ManagedThreadId}] 开始: {DateTime.Now:ss.fff}");
// 【见证奇迹时刻】
// 这里 await 的不是 Task,而是我们自定义的 WaitForSeconds
await new WaitForSeconds(2000);
Console.WriteLine($"[主线程:{Thread.CurrentThread.ManagedThreadId}] 结束: {DateTime.Now:ss.fff}");
Console.WriteLine("你看,不需要 Task 也能 await!");
// 验证带返回值的场景 (需要修改 Awaiter 泛型,此处略)
}
}
3. 原理解析:编译器做了什么?
当你写下:
var result = await new WaitForSeconds(2000);
C# 编译器实际上把它重写成了类似这样的状态机代码(伪代码):
// 1. 获取 Awaiter
var awaiter = new WaitForSeconds(2000).GetAwaiter();
// 2. 检查是否已完成
if (!awaiter.IsCompleted)
{
// 3. 如果没完成,注册回调(把后面的代码包起来)
// 这里的 "MoveNext" 是状态机继续执行的方法
awaiter.OnCompleted(MoveNext);
// 4. 返回,暂停当前方法(挂起)
return;
}
// 5. 如果已完成(或回调被触发后再次进入),获取结果
var result = awaiter.GetResult();
// 6. 继续执行后续代码...
4. 为什么要自定义 Awaitable Pattern?
既然 Task 这么好,为什么还要搞这么复杂?
-
性能优化 (
ValueTask):Task是类,每次创建都要分配堆内存。ValueTask是一个结构体实现的 Awaitable Pattern。对于高频调用且经常同步完成的场景(如读取内存缓存),使用ValueTask可以避免 GC 压力。 -
领域特定语言 (DSL):
在游戏开发(如 Unity 的UniTask)、机器人控制、工作流引擎中,你可能希望await具有特殊的语义。- 例如:
await new UntilClick()(等待直到用户点击)。 - 例如:
await new FrameDelay(5)(等待 5 帧,而不是毫秒)。
自定义 Awaitable 可以让代码读起来像自然语言。
- 例如:
-
桥接非 .NET 异步模型:
如果你需要对接一个古老的库,它使用“事件 + 回调”模式,你可以封装一个自定义 Awaitable,让老代码也能用async/await语法糖来调用,使代码更整洁。
总结
- Awaitable Pattern 是 C# 异步基石的底层协议。
- 核心三要素:
GetAwaiter()方法、IsCompleted属性、OnCompleted和GetResult方法。 - 日常开发:99% 的情况直接用
Task/ValueTask即可,无需自己实现该模式。 - 库开发者:如果你在编写高性能库或领域框架,理解并实现这个模式能让你提供比
Task更优的体验。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)