C#中有个很好用的东西,TAP异步编程(Task-based Asynchronous Pattern),是目前C#推荐的异步编程模型。它基于 System.Threading.Tasks.Task 和 async/await 关键字,旨在简化异步代码的编写、调试和维护。TAP 是现代 .NET 应用开发中最常用、最推荐的异步编程方式。
要实现TAP,有三个基本元素:Task、async和await。
- Task: 表示一个异步操作,可能没有返回值(
Task
)或有返回值(Task<T>
) - async: 标记一个方法为异步
- await: 用于等待一个异步操作完成,不会阻塞当前线程。
在Winform中,使用好TAP编程,可以方便的将异步回调操作变为同步,且不会阻塞UI线程! 非常优雅。
千问3给出的TAP优势:
特性 | 说明 |
---|---|
简化异步代码 | 使用 async/await 避免了复杂的回调嵌套(回调地狱)。 |
线程友好 | 不会阻塞主线程(如 UI 线程),提升用户体验。 |
异常处理统一 | 异常可以通过 try/catch 捕获,不需要额外的错误回调。 |
取消支持 | 支持通过 CancellationToken 安全取消异步操作。 |
上下文感知 | 通过 SynchronizationContext 自动恢复执行上下文(如 UI 线程) |
1.回调封装为异步
回调的关键是需要有key来识别是哪次调用!
通过TaskCompletionSource来封装回调执行时传输的结果数据。
通过await 一个Task实现阻塞等待。
基本代码如下:
public static class CallBackAsync{// 存储异步任务的上下文private static readonly ConcurrentDictionary<int, TaskCompletionSource<int>> _pendingTasks = new ConcurrentDictionary<int, TaskCompletionSource<int>>();// 回调中调用public static void OnCallBackAsync(int key, int result){if (_pendingTasks.TryRemove(key, out var tcs)){tcs.TrySetResult(result);//这里会激发await,并返回结果}}// 异步调用public static async Task<int> ChannelCtrlAsync(int key){var tcs = new TaskCompletionSource<int>();// 缓存任务上下文_pendingTasks[key] = tcs;// 调用原生同步方法int ret = NativeFunc(key);if (ret != 0){_pendingTasks.TryRemove(key, out _);tcs.TrySetException(new Exception($"{ret}")); //通过异常码返回错误!}return await tcs.Task; //关键点}// 原生方法public static int NativeFunc(int key){Console.WriteLine("NativeFunc");//异常情况//return -1;//线程中模拟回调Task.Run(() =>{OnCallBackAsync(1, 99);});return 0;}}
这里用ConcurrentDictionary字典封装了key和TaskCompletionSource的上下文信息,异步调用时要创建一份,回调时进行查找并删除。
注意当原生调用立即返回错误且不进回调时,产生的错误码没法通过TaskCompletionSource传出,此时可通过异常机制来输出。
按钮调用的实现:
private async void button1_Click(object sender, EventArgs e) //必须async标记{try{var result = await CallBackAsync.ChannelCtrlAsync(1).ConfigureAwait(true); // 恢复UI上下文//继续干其他事情,UI不会阻塞}catch (Exception ex){//表示底层调用直接返回了错误码Console.WriteLine($"底层直接返回,返回值:{ex.Message}");}}
2.阻塞调用封装
此时无需TaskCompletionSource去手动等待,比如串口的阻塞式异步:
public async Task SendDataAsync(string strText){return await Task.Run(() =>{m_Port.Write(strText);});}
3.任务取消
通过传入参数CancellationTokenSource 对象,可以在外部调用Cancel()方法取消任务。
public async Task SendXXXAsync(string strText, CancellationTokenSource cts){return await Task.Run(() =>{while(1){if (cts != null && cts.Token.IsCancellationRequested){return;}m_Port.Write(部分数据); //循环发送使用}});}
4.超时处理
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(3));
var completedTask = await Task.WhenAny(task, timeoutTask);
if (completedTask == timeoutTask)
{// 超时处理逻辑tcs.TrySetException(new Exception("ERR_CTRL_TIMEOUT")); //通过异常码返回错误!
}
return await tcs.Task;