前言
在 .NET 异步编程的演进中,Event‑based Asynchronous Pattern(EAP)------基于事件的异步模式------占据着承前启后的重要位置。它通过"启动异步方法 + 监听完成事件"的模型,在 Windows Forms、WPF 以及诸多 .NET Framework 组件中广泛应用。
尽管如今新项目更推荐 async/await 和 Task,但理解 EAP 对于维护遗留系统、使用传统组件、以及把握 .NET 异步编程的演变脉络仍然至关重要。
一、核心概念
1.1 什么是 EAP
EAP 的核心思想可概括为四步:
- 调用异步方法 ------ 启动一个异步操作
- 立即返回 ------ 不阻塞当前线程
- 触发完成事件 ------ 任务完成后自动触发事件
- 处理结果 ------ 在事件处理程序中获取结果、异常或取消状态
典型命名约定:
csharp
MethodAsync(); // 启动异步操作
MethodCompleted += MethodCompletedHandler; // 注册完成事件
1.2 一个直观的例子
csharp
WebClient client = new WebClient();
// 注册完成事件
client.DownloadStringCompleted += Client_DownloadStringCompleted;
// 启动异步下载
client.DownloadStringAsync(new Uri("https://example.com"));
DownloadStringAsync------ 启动异步下载DownloadStringCompleted------ 操作完成时触发的事件DownloadStringCompletedEventArgs------ 携带结果、异常和取消状态的事件参数
二、EAP 的标准结构
一个符合 EAP 规范的组件通常包含以下成员:
csharp
public void SomeMethodAsync(); // 异步方法
public event EventHandler<SomeMethodCompletedEventArgs> SomeMethodCompleted; // 完成事件
public void CancelAsync(); // 取消操作(可选)
2.1 事件参数基类
完成事件参数通常继承自 AsyncCompletedEventArgs,提供三个关键属性:
csharp
public class AsyncCompletedEventArgs : EventArgs
{
public Exception Error { get; } // 操作中发生的异常
public bool Cancelled { get; } // 是否被取消
public object UserState { get; } // 用户自定义状态标识
protected void RaiseExceptionIfNecessary(); // 如有异常则抛出
}
2.2 标准的事件处理模式
csharp
private void OperationCompleted(object sender, SomeMethodCompletedEventArgs e)
{
// 1. 检查取消
if (e.Cancelled) { /* 处理取消 */ return; }
// 2. 检查异常
if (e.Error != null) { /* 处理异常 */ return; }
// 3. 获取结果
var result = e.Result;
// 处理结果...
}
三、实战:WebClient 异步下载
3.1 基本用法
csharp
using System;
using System.Net;
class Program
{
static void Main()
{
using (WebClient client = new WebClient())
{
client.DownloadStringCompleted += OnDownloadCompleted;
Console.WriteLine("开始下载...");
client.DownloadStringAsync(new Uri("https://www.example.com"));
Console.WriteLine("主线程继续执行其他任务...");
Console.ReadLine();
}
}
private static void OnDownloadCompleted(object sender, DownloadStringCompletedEventArgs e)
{
if (e.Cancelled)
{
Console.WriteLine("下载已被取消。");
return;
}
if (e.Error != null)
{
Console.WriteLine($"下载失败:{e.Error.Message}");
return;
}
Console.WriteLine($"下载完成,内容长度:{e.Result.Length} 字符");
}
}
3.2 执行输出
开始下载...
主线程继续执行其他任务...
下载完成,内容长度:1256 字符
关键观察:"主线程继续执行其他任务..."在下载完成前即输出,证明异步操作未阻塞主线程。
四、异常处理
EAP 的异常不会 在调用 xxxAsync() 时直接抛出,而是通过完成事件的 e.Error 属性传递。
两种处理方式
方式一:显式检查属性(推荐)
csharp
if (e.Error != null)
{
Console.WriteLine($"失败:{e.Error.Message}");
return;
}
ProcessContent(e.Result);
方式二:调用 RaiseExceptionIfNecessary()
csharp
try
{
e.RaiseExceptionIfNecessary();
ProcessContent(e.Result);
}
catch (Exception ex)
{
Console.WriteLine($"操作失败:{ex.Message}");
}
最佳实践 :大多数情况下,显式检查
Cancelled和Error更为清晰可控。
五、取消操作
5.1 取消机制
EAP 的取消是协作式 的:组件提供 CancelAsync() 方法,后台任务需主动检查取消标志并退出。
csharp
client.DownloadStringAsync(new Uri("https://www.example.com"));
if (Console.ReadKey().Key == ConsoleKey.C)
{
client.CancelAsync(); // 发送取消请求(非即时)
}
在完成事件中:
csharp
if (e.Cancelled)
{
Console.WriteLine("下载已取消。");
return;
}
5.2 取消的重要特性
- 非即时生效 :
CancelAsync()仅设置标志,不会立即终止操作 - 完成事件仍触发 :取消后完成事件依然触发,但
e.Cancelled == true - 协作式 :后台任务需定期检查
CancellationPending并自主退出
六、Windows Forms 中的 EAP:BackgroundWorker 实战
BackgroundWorker 是 EAP 在 UI 编程中的经典代表,完美演示了进度报告和跨线程更新 UI。
完整示例(WinForms)
假设窗体包含:btnStart、btnCancel、progressBar1、lblStatus。
csharp
public partial class MainForm : Form
{
private readonly BackgroundWorker _worker;
public MainForm()
{
InitializeComponent();
_worker = new BackgroundWorker
{
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
_worker.DoWork += Worker_DoWork;
_worker.ProgressChanged += Worker_ProgressChanged;
_worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
btnStart.Click += BtnStart_Click;
btnCancel.Click += BtnCancel_Click;
btnCancel.Enabled = false;
}
private void BtnStart_Click(object sender, EventArgs e)
{
if (_worker.IsBusy) return;
progressBar1.Value = 0;
lblStatus.Text = "正在执行...";
btnStart.Enabled = false;
btnCancel.Enabled = true;
_worker.RunWorkerAsync();
}
private void BtnCancel_Click(object sender, EventArgs e)
{
if (_worker.IsBusy) _worker.CancelAsync();
}
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
var worker = (BackgroundWorker)sender;
for (int i = 0; i <= 100; i++)
{
if (worker.CancellationPending) { e.Cancel = true; return; }
Thread.Sleep(50);
worker.ReportProgress(i);
}
e.Result = "处理成功";
}
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
lblStatus.Text = $"进度:{e.ProgressPercentage}%";
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
btnStart.Enabled = true;
btnCancel.Enabled = false;
if (e.Cancelled)
lblStatus.Text = "操作已取消";
else if (e.Error != null)
lblStatus.Text = $"失败:{e.Error.Message}";
else
lblStatus.Text = e.Result?.ToString() ?? "完成";
}
}
执行流程
用户点击"开始"
→ RunWorkerAsync()
→ DoWork(新线程)
→ 循环中 ReportProgress()
→ ProgressChanged(UI线程)
→ 循环结束
→ RunWorkerCompleted(UI线程)
七、自定义 EAP 组件
7.1 定义完成事件参数
csharp
public class CalculateCompletedEventArgs : AsyncCompletedEventArgs
{
public int Result { get; }
public CalculateCompletedEventArgs(int result, Exception error, bool cancelled, object userState)
: base(error, cancelled, userState) => Result = result;
}
7.2 实现异步计算器
csharp
public class Calculator
{
private bool _cancellationPending;
private readonly object _lock = new object();
public event EventHandler<CalculateCompletedEventArgs> CalculateCompleted;
public void CalculateAsync(int number, object userState = null)
{
lock (_lock) _cancellationPending = false;
ThreadPool.QueueUserWorkItem(_ =>
{
int result = 0;
Exception error = null;
bool cancelled = false;
try
{
for (int i = 0; i <= number; i++)
{
lock (_lock) if (_cancellationPending) { cancelled = true; break; }
result += i;
Thread.Sleep(100);
}
}
catch (Exception ex) { error = ex; }
OnCalculateCompleted(result, error, cancelled, userState);
});
}
public void CancelAsync()
{
lock (_lock) _cancellationPending = true;
}
private void OnCalculateCompleted(int result, Exception error, bool cancelled, object userState)
{
var args = new CalculateCompletedEventArgs(result, error, cancelled, userState);
CalculateCompleted?.Invoke(this, args);
}
}
7.3 使用自定义组件
csharp
var calc = new Calculator();
calc.CalculateCompleted += (s, e) =>
{
if (e.Cancelled) Console.WriteLine("已取消");
else if (e.Error != null) Console.WriteLine($"失败:{e.Error.Message}");
else Console.WriteLine($"结果:{e.Result}");
};
calc.CalculateAsync(100);
Console.WriteLine("异步已启动,主线程未阻塞。");
输出:
异步已启动,主线程未阻塞。
结果:5050
八、高级实现:使用 AsyncOperationManager
对于生产级组件,推荐使用 AsyncOperationManager 和 AsyncOperation,它们能自动处理同步上下文,确保完成事件在正确的线程(如 UI 线程)触发。
csharp
public class ProfessionalCalculator
{
private bool _cancellationPending;
private readonly object _sync = new object();
public event EventHandler<CalculateCompletedEventArgs> CalculateCompleted;
public void CalculateAsync(int number, object userState = null)
{
AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(userState);
lock (_sync) _cancellationPending = false;
ThreadPool.QueueUserWorkItem(_ =>
{
int result = 0;
Exception error = null;
bool cancelled = false;
try
{
for (int i = 0; i <= number; i++)
{
lock (_sync) if (_cancellationPending) { cancelled = true; break; }
result += i;
Thread.Sleep(50);
}
}
catch (Exception ex) { error = ex; }
var args = new CalculateCompletedEventArgs(result, error, cancelled, userState);
asyncOp.PostOperationCompleted(_ => CalculateCompleted?.Invoke(this, args), args);
});
}
public void CancelAsync() { lock (_sync) _cancellationPending = true; }
}
关键点:
AsyncOperationManager.CreateOperation(userState)捕获当前同步上下文PostOperationCompleted确保委托在正确的上下文中执行(如 UI 线程)
九、用户状态管理
当需要同时发起多个异步操作时,通过 userState 参数区分任务:
csharp
calculator.CalculateAsync(100, "TaskA");
calculator.CalculateAsync(200, "TaskB");
在完成事件中识别:
csharp
string taskName = e.UserState?.ToString() ?? "未知";
if (e.Cancelled) Console.WriteLine($"[{taskName}] 已取消");
else if (e.Error != null) Console.WriteLine($"[{taskName}] 失败");
else Console.WriteLine($"[{taskName}] 结果:{e.Result}");
十、EAP 的优缺点
| 优点 | 说明 |
|---|---|
| 事件驱动自然 | 完美适配 WinForms/WPF 的事件模型 |
| 不阻塞 UI | 后台执行,界面保持响应 |
| 内置进度报告 | 通过进度事件轻松更新 UI |
| 标准化取消 | 提供 CancelAsync 机制 |
| 向后兼容 | 大量旧项目和组件仍在使用 |
| 缺点 | 说明 |
|---|---|
| 回调嵌套 | 多步操作易形成"回调地狱" |
| 异常处理不直观 | 通过事件参数传递,无法使用 try‑catch |
| 结果获取繁琐 | 需从事件参数中提取 |
| 取消非强制 | 协作式,需手动检查标志 |
| 内存泄漏风险 | 事件订阅可能导致对象无法回收 |
十一、EAP vs TAP 对比
| 对比维度 | EAP(基于事件) | TAP(基于任务) |
|---|---|---|
| 核心机制 | 事件 | Task<T> |
| 结果获取 | e.Result |
await 返回值 |
| 异常处理 | e.Error 属性 |
try-catch |
| 取消方式 | CancelAsync() |
CancellationToken |
| 代码风格 | 事件驱动 | 线性同步风格 |
| 组合操作 | 手动管理,复杂 | Task.WhenAll/WhenAny 等 |
| 进度报告 | 进度事件 | IProgress<T> |
| 可读性 | 嵌套较多 | 接近同步代码 |
| 适用场景 | 传统项目、旧组件 | 新项目、现代开发 |
十二、何时使用 EAP?
推荐使用 EAP 的场景
- 维护遗留系统(如使用
BackgroundWorker、WebClient的项目) - 使用仍提供 EAP 接口的第三方库
- 学习异步编程演变历史
- 极简单的异步操作,无需复杂组合
推荐使用 TAP 的场景
- 全新项目开发,优先使用
async/await - 复杂的异步流程(组合、并发控制)
- 需要精细控制(
CancellationToken、IProgress<T>) - 高性能场景(
ValueTask)
EAP → TAP 迁移桥接
使用 TaskCompletionSource<T> 将 EAP 包装为 TAP:
csharp
public static Task<string> DownloadStringTaskAsync(this WebClient client, Uri uri)
{
var tcs = new TaskCompletionSource<string>();
client.DownloadStringCompleted += (s, e) =>
{
if (e.Cancelled) tcs.TrySetCanceled();
else if (e.Error != null) tcs.TrySetException(e.Error);
else tcs.TrySetResult(e.Result);
};
client.DownloadStringAsync(uri);
return tcs.Task;
}
// 使用
string content = await client.DownloadStringTaskAsync(uri);
十三、开发 EAP 组件的最佳实践
命名规范
csharp
// ✅ 正确
public void DownloadAsync();
public event EventHandler<DownloadCompletedEventArgs> DownloadCompleted;
public void CancelAsync();
// ❌ 错误
public void StartDownload();
public event EventHandler<EventArgs> Finished;
public void Stop();
线程安全
csharp
private readonly object _lock = new object();
private bool _isBusy;
public void StartAsync()
{
lock (_lock)
{
if (_isBusy) throw new InvalidOperationException("操作正在进行");
_isBusy = true;
}
// 启动异步操作...
}
资源清理
csharp
public class MyComponent : IDisposable
{
public void Dispose()
{
CancelAsync();
MyCompleted = null; // 释放事件引用
}
}
完整错误检查顺序
csharp
private void OnCompleted(object sender, MyCompletedEventArgs e)
{
if (e.Cancelled) { /* 取消处理 */ return; }
if (e.Error != null) { /* 错误处理 */ return; }
// 正常处理结果
ProcessResult(e.Result);
}
十四、总结
核心模式回顾
MethodAsync() + MethodCompleted 事件 + MethodCompletedEventArgs
在完成事件中通过三个关键属性判断结果:
e.Result------ 操作结果e.Error------ 异常信息e.Cancelled------ 是否取消
典型代表
BackgroundWorker------ WinForms 后台任务WebClient------ HTTP 请求SoundPlayer------ 音频播放PictureBox------ 图像加载
总结
EAP 通过事件通知异步操作完成,TAP 通过 Task 表示异步操作。EAP 更适合传统代码,TAP 更适合现代 C# 开发。
对于新项目,请优先选择基于 Task 的异步模式;对于旧项目维护,理解 EAP 能帮助你正确使用现有组件。掌握这两种模式,你就能从容应对 .NET 生态中的各种异步编程场景。