.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 命名空间中的 TaskTask<TResult> 类型。

核心概念

TAP 是 C#异步编程的黄金标准,其三大核心要素:

  • Task/Task:统一异步操作容器
    • 提供标准化的异常传播机制(AggregateException 封装)
    • 支持取消令牌(CancellationToken)和进度报告(IProgress
  • async/await 语法糖
    • 保留同步代码结构的异步语义
    • 自动生成状态机代码(后续详解)
  • ExecutionContext 流式传输
    • 保持异步上下文
    • 通过 AsyncLocal显示跨 await 边界数据传递

TAP 与 EAP 和 APM 模式的区别

  • TAP 使用单个方法表示异步操作的开始和完成。
  • EAP 需要后缀为 Async 的方法,以及一个或多个事件、事件处理程序委托类型和 EventArg 派生类型。
  • APM 需要 BeginEnd 方法。

TAP 中的异步方法命名规则

  1. TAP 中的异步方法在返回可等待类型(如 TaskTask<TResult>ValueTaskValueTask<TReult>)的方法的操作名称后面添加 Async 后缀。
Task DoAsync();
  1. 若要将 TAP 方法添加到已包含带 Async 后缀的 EAP 方法名称的类型,改用后缀 TaskAsync。
Task DoTaskAsync();
  1. 如果方法启动异步操作,但不返回可等待类型,它的名称应以 BeginStart或表明此方法不返回或抛出操作结果的其他某谓词开头。
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 方法的参数应与其同步方法的参数一致,但是 outref 参数不受此规则的限制,并应完全避免。

应该将通过 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 关键字转换为同步的底层实现。

原理:将异步方法转换为状态机,以管理异步操作的挂起、恢复和上下文切换。

原理

原始async方法
语法解析
是否包含await
生成状态机类
编译警告CS1998
IL代码生成
JIT编译优化

举例

假设有一个异步方法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> 对象、执行异步操作,并在操作完成时,调用 SetResultSetExceptionSetCanceled 等方法,或调用这些方法之一的 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

引用

  1. 微软官方文档 - 异步编程模式
    https://learn.microsoft.com/zh-cn/dotnet/standard/asynchronous-programming-patterns/

声明

内容准确性: 我会尽力确保所分享信息的准确性和可靠性,但由于个人知识有限,难免会有疏漏或错误。如果您在阅读过程中发现任何问题,请不吝赐教,我将及时更正。

AI: 文章部分代码参考了DeepSeek和ChatGTP大语言模型生成的内容。

posted on   wubing7755  阅读(3)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· Blazor Hybrid适配到HarmonyOS系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示