c# 异步编程模型 APM EAP TAP 的区别

来源:这里教程网 时间:2026-02-21 17:35:44 作者:

APM 是什么,现在还该用吗

APM(Asynchronous Programming Model)是 .NET 早期的异步模式,基于

IAsyncResult
接口,靠
BeginXXX
/
EndXXX
成对方法实现。比如
FileStream.BeginRead
FileStream.EndRead

它的问题很实际:回调嵌套深、异常处理分散、取消逻辑难统一、资源释放容易出错。.NET Core 2.0+ 已不再为新 API 添加 APM 支持,新项目完全不该选用 APM

不支持
async
/
await
语法,无法自然融入现代 C# 异步流
EndXXX
必须调用,否则可能丢失异常或阻塞资源(如未调用
EndRead
可能导致句柄泄漏)
没有内置取消机制,需手动传入
CancellationToken
并在回调中检查

EAP 在 WinForms/WPF 中为什么还能见到

EAP(Event-based Asynchronous Pattern)以

MethodNameAsync
+
MethodNameCompleted
事件形式存在,典型如
WebClient.DownloadStringAsync
DownloadStringCompleted
事件。

它主要服务于 UI 框架的线程模型,自动把完成回调封送到 UI 线程(通过

SynchronizationContext
),所以老式 WinForms/WPF 代码里还能看到。但它的设计本质是“为 UI 而妥协”,不是通用异步抽象。

不返回
Task
,无法用
await
,也不能参与
Task.WhenAll
等组合操作
错误和结果都打包在
AsyncCompletedEventArgs
Error
Result
属性里,类型擦除严重(
Result
object
.NET 5+ 已标记多数 EAP 方法为
[Obsolete]
,比如
SmtpClient.SendAsync

TAP 是唯一推荐的现代异步模式

TAP(Task-based Asynchronous Pattern)是当前 C# 异步的标准,所有新 API 都遵循它:方法名以

Async
结尾,返回
Task
Task<t></t>
,支持
await
,原生集成取消、进度报告和同步上下文。

它不是“另一种选择”,而是事实标准。哪怕你封装一个旧 APM 方法,也该用

TaskFactory.FromAsync
转成 TAP;调用 EAP 方法,也该用
TaskCompletionSource<t></t>
包一层。

async
方法体内可直接
await
任何 TAP 方法,包括你自己写的
async Task<string> GetDataAsync()</string>
统一用
CancellationToken
控制取消,且绝大多数 TAP 方法都提供带 token 的重载(如
HttpClient.GetAsync(uri, token)
异常会原样抛出(非包装进
AggregateException
),调试体验好
public async Task<string> FetchDataAsync(string url, CancellationToken ct = default)
{
    using var client = new HttpClient();
    var response = await client.GetAsync(url, ct); // 自动响应取消
    return await response.Content.ReadAsStringAsync(ct);
}

混用三种模式时最容易踩的坑

真实项目常要对接老代码或第三方库,可能同时遇到三种模式。这时最危险的操作是“假装它们等价”——比如把

BeginInvoke
回调里直接调
EndInvoke
,却不考虑同步上下文;或给 EAP 事件 handler 加
async void
导致异常无法捕获。

不要在 EAP 的
Completed
事件处理器里写
async void
—— 改用
async Task
并手动调用
await
后续逻辑,否则异常会直接崩掉进程
Task.Factory.FromAsync
包装 APM 时,必须确保
EndXXX
被调用 —— 它内部已帮你做了,但自定义包装时容易漏
从 TAP 切回同步调用(如
task.Result
task.Wait()
)极易引发死锁,尤其在有
SynchronizationContext
的环境(WinForms/WPF/ASP.NET MVC)

异步模型的差异不在语法糖,而在控制流所有权和错误传播路径。选错起点,后面每一步都在补洞。

相关推荐