.NET 异步编程TAP模式简述
.NET 异步编程TAP模式简述
概述
根据官方文档中的介绍,.NET 异步编程主要分为三种模式:
- 基于任务的异步模式(Task-based Asynchronous Pattern, TAP)
- TAP 是在 .NET Framework 4 中引入的,这是在 .NET 中进行异步编程的推荐方法。
- 基于事件的异步模式(Event-based Asynchronous Pattern, EAP)
- EAP 是在 .NET Framework 2 中引入的,不建议新开发中使用这种模式。
- 异步编程模型模式(Asynchronous Programming Model, APM)
- 不建议新开发中使用这种模式。
其中,TAP模式基于 System.Treading.Tasks
命名空间中的 Task
和 Task<TResult>
类型。
核心概念
TAP 是 C#异步编程的黄金标准,其三大核心要素:
- Task/Task
:统一异步操作容器 - 提供标准化的异常传播机制(AggregateException 封装)
- 支持取消令牌(CancellationToken)和进度报告(IProgress
)
- async/await 语法糖
- 保留同步代码结构的异步语义
- 自动生成状态机代码(后续详解)
- ExecutionContext 流式传输
- 保持异步上下文
- 通过 AsyncLocal
显示跨 await 边界数据传递
TAP 与 EAP 和 APM 模式的区别
- TAP 使用单个方法表示异步操作的开始和完成。
- EAP 需要后缀为
Async
的方法,以及一个或多个事件、事件处理程序委托类型和EventArg
派生类型。 - APM 需要
Begin
和End
方法。
TAP 中的异步方法命名规则
- TAP 中的异步方法在返回可等待类型(如
Task
、Task<TResult>
、ValueTask
和ValueTask<TReult>
)的方法的操作名称后面添加Async
后缀。
Task DoAsync();
- 若要将 TAP 方法添加到已包含带
Async
后缀的 EAP 方法名称的类型,改用后缀 TaskAsync。
Task DoTaskAsync();
- 如果方法启动异步操作,但不返回可等待类型,它的名称应以
Begin
、Start
或表明此方法不返回或抛出操作结果的其他某谓词开头。
Task DoBegin(); Task DoStart();
TAP 方法的返回参数类型
TAP 方法返回参数类型为:
System.Threading.Tasks.Task
System.Threading.Tasks.Task<TResult>
具体取决于相应同步方法返回的是 void 还是类型 TResult。
- 同步方法返回 void
// 同步方法 public void WriteToFile(string filePath, string content) { using(StreamWriter writer = new StreamWriter(filePath)) { writer.WriteLine(content); } } // 异步方法 public async Task WriteToFileAsync(string filePath, string content) { using(StreamWriter writer = new StreamWriter(filePath)) { await writer.WriteLineAsync(content); } }
- 同步方法返回 TResult
// 同步方法 public string ReadFromFile(string filePath) { using(StreamReader reader = new StreamReader(filePath)) { return reader.ReadToEnd(); } } // 异步方法 public async Task<string> ReadFromFileAsync(string filePath) { using(StreamReader reader = new StreamReader(filePath)) { return await reader.ReadToEndAsync(); } }
TAP 方法的方法参数
TAP 方法的参数应与其同步方法的参数一致,但是 out
和 ref
参数不受此规则的限制,并应完全避免。
应该将通过 out 或 ref 参数返回的所有数据改为作为由 TResult 返回的 Task<TResult>
的一部分返回,且应使用元组或自定义数据结构来容纳多个值。
即使 TAP 方法的同步对应项没有提供 CancelationToken 参数,也应考虑添加此参数。
- out 参数
public void FetchData(string id, out int value, out string message) { value = 42; message = "Data fetched successfully"; } public async Task<(int value, string message)> FetchDataAsync(string id) { // 模拟异步操作 await Task.Delay(1000); return (42, "Data fetched successfully"); }
- ref 参数
public void UpdateValue(ref int value) { value += 10; } public async Task<int> UpdateValueAsync(int value) { // 模拟异步操作 await Task.Delay(1000); return value + 10; }
- 添加 CancellationToken 参数
public void ProcessData(List<int> data, CancellationToken cancellationToken) { foreach (var item in data) { // 检查取消请求 cancellationToken.ThrowIfCancellationRequested(); // 模拟耗时操作 Thread.Sleep(1000); Console.WriteLine(item); } } public async Task ProcessDataAsync(List<int> data, CancellationToken cancellationToken) { using(cancellationToken.Register(() => Console.WriteLine("Cancellation requested"))) { // 检查取消请求 cancellationToken.ThrowIfCancellationRequested(); // 模拟异步操作 await Task.Delay(1000, cancellationToken); } }
TAP 的三种实现方式
实现方式为以下三种:
- 编译器支持(C#和 Visual Basic 编译器)
- 手动实现
- 编译器支持和手动实现相结合
编译器支持
编译器会自动将 async/await 关键字转换为同步的底层实现。
原理:将异步方法转换为状态机,以管理异步操作的挂起、恢复和上下文切换。
原理
举例
假设有一个异步方法DownloadStringAsync
从指定的 URL(https://example.com
)下载网页内容,并将该内容作为字符串返回。
public async Task<string> DownloadStringAsync() { using HttpClient client = new HttpClient(); string content = await client.GetStringAsync("https://example.com"); return content; }
编译器会将其转换为以下状态机:
[CompilerGenerated] // 标记为编译器生成的代码 private sealed class <DownloadStringAsync>d__1 : /*[Nullable(0)]*/ IAsyncStateMachine // IAsyncStateMachine接口:定义了MoveNext()和SetStateMachine()方法 { /* * 成员变量: * <>1__state:表示状态机的当前状态,其值决定了方法执行的进度(开始、挂起、完成等)。 * <>t__builder:表示异步方法的返回任务对象(Task<string>类型)的构建器。 * <client>5__1:表示 HttpClient 类型的实例。 * <content>5__2:表示返回的网页内容。 * <>s__3:表示临时存储从await表达式返回的值。 * <>u__1:表示GetStringAsync()返回的TaskAwaiter<string>类型的实例,用于等待异步操作的完成。 */ public int <>1__state; // 状态标识:0 = 等待中,-1=初始状态,-2=已完成 [Nullable(0)] public AsyncTaskMethodBuilder<string> <>t__builder; [Nullable(0)] private HttpClient <client>5__1; [Nullable(0)] private string <content>5__2; [Nullable(0)] private string <>s__3; [Nullable(new byte[] {0, 1})] private TaskAwaiter<string> <>u__1; /* * 构造函数: * base..ctor():调用基类的构造函数。 */ public <DownloadStringAsync>d__1() { base..ctor(); } /* * MoveNext()方法: * 状态机的核心逻辑,控制状态机的执行流程,处异 */ void IAsyncStateMachine.MoveNext() { // 获取当前状态机的状态 int num = this.<>1__state; // 最终返回的结果 string content52; try { // 首次执行(非恢复状态) if (num != 0) { // 对应原始方法中的 using HttpClient client = new HttpClient(); this.<client>5__1 = new HttpClient(); } try { TaskAwaiter<string> awaiter; // 首次执行路径 if (num != 0) { // 开始异步操作并获取等待器 awaiter = this.<client>5__1.GetStringAsync("https://example.com").GetAwaiter(); // 异步操作未立即完成 if (!awaiter.IsCompleted) { // 更新状态为等待中 this.<>1__state = num = 0; // 保持等待器实例 this.<>u__1 = awaiter; // 注册回调,在异步操作完成后调用MoveNext()方法 Program.<DownloadStringAsync>d__1 stateMachine = this; this.<>t__builder. AwaitUnsafeOnCompleted<TaskAwaiter<string>, Program.<DownloadStringAsync>d__1>(ref awaiter, ref stateMachine); // 挂起方法,交还控制权 return; } } // 从挂起状态恢复 else { // 获取保存的等待器 awaiter = this.<>u__1; // 重置等待器防止内存泄漏 this.<>u__1 = new TaskAwaiter<string>(); // 标记为运行中状态 this.<>1__state = num = -1; } // 获取异步操作结果 this.<>s__3 = awaiter.GetResult(); // 赋值给content变量 this.<content>5__2 = this.<>s__3; // 清楚临时引用 this.<>s__3 = (string) null; // 准备返回值 content52 = this.<content>5__2; } finally { // 仅在非挂起状态时,释放资源 if (num < 0 && this.<client>5__1 != null) { // 释放 HttpClient 资源 this.<client>5__1.Dispose(); } } } catch (Exception ex) { // 清理状态机状态,标记为 -2=已完成 this.<>1__state = -2; // 释放引用 this.<client>5__1 = (HttpClient) null; this.<content>5__2 = (string) null; // 将异常传播到Task this.<>t__builder.SetException(ex); return; } // 设置结果并标记状态机为 -2=已完成 this.<>1__state = -2; this.<client>5__1 = (HttpClient) null; this.<content>5__2 = (string) null; // 设置 Task 的最终结果 this.<>t__builder.SetResult(content52); } /* * SetStateMachine方法: * 用于设置状态机的上下文。 */ [DebuggerHidden] void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { } }
手动实现
不依赖于编译器(即,在不使用 async/await 关键字的情况下),直接使用 Task 和 Task
方法
如要自己实现 TAP,你需要创建一个 TaskCompletionSource<TResult>
对象、执行异步操作,并在操作完成时,调用 SetResult
、SetException
、SetCanceled
等方法,或调用这些方法之一的 Try 版本。
举例
以下是一个手动实现异步延时的方式:创建任务->启动异步操作->完成任务。
public Task DelayAsync(int millisecondsDelay) { var tcs = new TaskCompletionSource<object>(); Timer timer = null; timer = new Timer(_ => { // 立即释放 Timer 资源 timer.Dispose(); // 标记任务完成 tcs.TrySetResult(null); }, null, millisecondsDelay, Timeout.Infinite); return tcs.Task; }
适用场景
- 旧代码改造:将 EAP/APM 模式的代码升级为 TAP 模式
- 精细控制任务生命周期
- 减少分配开销
- 非标准异步场景
编译器支持和手动实现结合
既有编译器对 async/await 的支持,又有手动实现的灵活性。
简而言之,手动控制任务的创建和完成,同时依靠编译器处理一些底层的异步机制。
举例
以下是一个手动是实现延迟操作,并结合 async/await 进行异步编程的例子。
public Task<int> CalculateAferDelayAsync(int delayMilliseconds, int number) { var tcs = new TaskCompletionSource<int>(); // 模拟延迟操作 Task.Run(async () => { try { await Task.Delay(delayMilliseconds); int result = number * number; // 标记任务完成 tcs.SetResult(result); } catch(Exception ex) { // 标记任务失败 tcs.SetException(ex); } }); return tcs.Task; }
TAP 的用处
用 TAP 模式实现计算密集型和 I/O 密集型异步操作。
- 计算密集型:CPU高负载
- I/O密集型:等待外部资源
计算密集型
System.Threading.Tasks.Task 类非常适合表示计算密集型操作。
默认情况下,它利用 ThreadPool 类中的特殊支持来提供有效的执行,还对执行异步计算的时间、地点和方式提供重要控制。
举例:实现一个异步方法 IsPrimeAsync
,用于判断大数是否为质数。
public Task<bool> IsPrimeAsync(long number, CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource<bool>(); // 使用ThreadPool避免阻塞调用线程 ThreadPool.QueueUserWorkItem(_ => { try { cancellationToken.ThrowIfCancellationRequested(); bool result = IsPrime(number, cancellationToken); tcs.TrySetResult(result); } catch(OperationCanceledException) { tcs.TrySetCanceled(); } catch(Exception ex) { tcs.TrySetException(ex); } }); return tcs.Task; } private bool IsPrime(long n, CancellationToken cancellationToken) { if (n <= 1) return false; if (n == 2) return true; if (n % 2 == 0) return false; for (long i = 3; i <= Math.Sqrt(n); i += 2) { cancellationToken.ThrowIfCancellationRequested(); // 检查取消请求 if (n % i == 0) return false; } return true; }
I/O密集型
举例:实现一个异步方法 CopyFileAsync
,用于实现文件复制。
public async Task CopyFileAsync(string sourcePath, string destPath, CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource<object>(); // 异步读取和写入 try { // 打开文件流(异步模式) using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, 81920, FileOptions.Asynchronous)) using (FileStream destStream = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None, 81920, FileOptions.Asynchronous)) { byte[] buffer = new byte[81920]; // 80KB 缓冲区 // 注册取消操作 cancellationToken.Register(() => { tcs.TrySetCanceled(); }); int bytesRead; // 异步读取并写入数据 while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) { await destStream.WriteAsync(buffer, 0, bytesRead, cancellationToken); } tcs.TrySetResult(null); // 文件复制完成 } } catch (Exception ex) { tcs.TrySetException(ex); // 处理异常 } }
挂起、恢复、取消、监视异步操作
使用 await 挂起执行
举例:遇到 await 关键字时,方法将控制权返回给调用方,但 Task.Delay 完成后会恢复执行。
[Fact] public async void TestAsyncMethod() { Console.WriteLine("0. ------ 调用异步方法前 ------ "); Task task = AsyncMethod(); Console.WriteLine("2. ------ 异步方法返回Task,但未完成 ------ "); await task; Console.WriteLine("4. ------ 异步方法完成 ------ "); } public async Task AsyncMethod() { Console.WriteLine("1. ------ 同步执行开始 ------ "); await Task.Delay(3000); Console.WriteLine("3. ------ 3秒后恢复执行 ------ "); }
运行结果:

使用 Yield 和 ConfigureAwait 配置挂起和恢复
Task.Yield
Task.Yield 会让当前任务在执行过程中“暂停”,并放弃当前线程的控制,直到调度器决定重新开始执行这个任务。
举例:在Task.Yield()之前,启动一个线程池任务。
[Fact] public async Task TestYieldAsync() { Console.WriteLine("0. ------ 异步方法开始执行 ------"); await Task.Delay(3000); Console.WriteLine("1. ------ Before Yield ------"); // 启动一个线程池任务 var poolTask = Task.Run(() => { Console.WriteLine("ThreadPool Task: 开始执行"); Thread.Sleep(3000); // 模拟线程池任务的执行 Console.WriteLine("ThreadPool Task: 执行完成"); }); await Task.Yield(); Console.WriteLine("2. ------ After Yield ------"); await Task.Delay(3000); Console.WriteLine("3. ------ 异步方法执行完成 ------"); await poolTask; // 确保线程池任务完成 }
运行结果:

注释掉 await Task.Yield();语句
[Fact] public async Task TestYieldAsync() { Console.WriteLine("0. ------ 异步方法开始执行 ------"); await Task.Delay(3000); Console.WriteLine("1. ------ Before Yield ------"); // 启动一个线程池任务 var poolTask = Task.Run(() => { Console.WriteLine("ThreadPool Task: 开始执行"); Thread.Sleep(3000); // 模拟线程池任务的执行 Console.WriteLine("ThreadPool Task: 执行完成"); }); //await Task.Yield(); Console.WriteLine("2. ------ After Yield ------"); await Task.Delay(3000); Console.WriteLine("3. ------ 异步方法执行完成 ------"); await poolTask; // 确保线程池任务完成 }
运行结果:

Task.ConfigureAwait
Task.ConfigureAwait 用来控制一个异步操作完成后是否继续在原线程上执行后续代码。
举例:在异步操作完成后不再返回到UI线程,而是在后台线程中继续处理。
[Fact] public async Task TestConfigureAwaitAsync() { Console.WriteLine("Start of the method."); // 异步操作,使用 ConfigureAwait(false) 不在原线程恢复 await Task.Delay(1000).ConfigureAwait(false); Console.WriteLine("After Delay, running on a different thread if possible."); // 这里的代码不再依赖于原始同步上下文(UI 线程),可以继续在线程池线程上执行 await Task.Delay(1000); Console.WriteLine("End of the method."); }
取消异步操作
通过 CancellationToken 和 CancellationTokenSource,可以在异步操作中添加取消功能,允许用户在操作进行中中断它。
举例:使用 CancellationToken 来取消一个长时间运行的异步操作
[Fact] public async void TestLongRunningTask() { // 创建取消令牌源 var cts = new CancellationTokenSource(); // 启动一个长时间运行的异步任务,传递取消令牌 var task = LongRunningTask(cts.Token); // 模拟在 3 秒后取消任务 await Task.Delay(3000); // 等待 3 秒 cts.Cancel(); // 触发取消 // 等待任务完成 await task; Console.WriteLine("程序结束"); } public async Task LongRunningTask(CancellationToken token) { Console.WriteLine("任务开始执行..."); for (int i = 0; i < 5; i++) { // 每 1 秒检查一次是否取消 await Task.Delay(1000); if (token.IsCancellationRequested) { Console.WriteLine("任务被取消!"); return; // 如果请求取消,则退出任务 } Console.WriteLine($"执行中... 第 {i + 1} 秒"); } Console.WriteLine("任务完成!"); }
取消操作的优点:
- 可以将相同的取消令牌传递给多个异步和同步操作。
- 取消请求可以扩展到多个异步操作,确保统一取消。
- 开发者可以完全控制操作是否支持取消以及何时生效。
- 使用取消令牌可以灵活地指定哪些操作应该响应取消请求
监视进度
某些异步方法通过传入异步方法的进度接口来公开进度。
[Fact] public async void TestDownloadStringAsync() { var url = @"https://example.com"; var progress = new Progress<int>(p => Console.WriteLine($"下载进度: {p}%")); var result = await DownloadStringAsync(url, progress); _testOutputHelper.WriteLine($"Result: ------ {result} ------ "); } public async Task<string> DownloadStringAsync(string url, IProgress<int> progress) { using (var client = new HttpClient()) { var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); var totalBytes = response.Content.Headers.ContentLength.GetValueOrDefault(); var buffer = new byte[8192]; var bytesRead = 0; var totalBytesRead = 0L; using (var stream = await response.Content.ReadAsStreamAsync()) { while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) { totalBytesRead += bytesRead; // 计算下载进度并更新 int progressPercentage = (int)((totalBytesRead * 100) / totalBytes); progress?.Report(progressPercentage); } } return totalBytesRead.ToString(); } }
使用内置的基于任务的连结符
System.Threading.Tasks 命名空间包含多个方法,可用于撰写和处理任务。
Task.Run
使用方法如下:
Task.Run(() => { // 模拟异步操作 await Task.Delay(10); return 0; });
等同于
Task<Task<int>> innerTask = Task.Factory.StartNew<Task<int>>(async delegate { // 执行异步操作 await Task.Delay(10); return 0; }, default, TaskCreationOptions.None, TaskScheduler.Default); Task<int> task = innerTask.Unwrap();
以下通过计算斐波那契数列来说明 Task.Run的使用及其于 TaskFactory.StartNew 的关系。
[Fact] public async void TestCalculateFibonacciAsync() { // 使用 Task.Run 启动一个异步任务 int result = await Task.Run(async () => { return await CalculateFibonacciAsync(10); }); } public async Task<int> CalculateFibonacciAsync(int n) { if(n <= 1) { return n; } // 模拟异步操作 await Task.Delay(10); return await CalculateFibonacciAsync(n-1) + await CalculateFibonacciAsync(n-2); }
------ 未完待续 ------
Task.FromResult
Task.WhenAll
Task.WhenAny
构建基于任务的连接符
RetryOnFault
NeedOnlyOne
WhenAllOrFirstException
构建基于任务的数据结构
AsyncCache
AsyncProducerConsumerCollection
引用
- 微软官方文档 - 异步编程模式
https://learn.microsoft.com/zh-cn/dotnet/standard/asynchronous-programming-patterns/
声明
内容准确性: 我会尽力确保所分享信息的准确性和可靠性,但由于个人知识有限,难免会有疏漏或错误。如果您在阅读过程中发现任何问题,请不吝赐教,我将及时更正。
AI: 文章部分代码参考了DeepSeek和ChatGTP大语言模型生成的内容。
posted on 2025-02-18 16:11 wubing7755 阅读(3) 评论(0) 编辑 收藏 举报
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Blazor Hybrid适配到HarmonyOS系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库