C# 異步回調(diào)與等待機(jī)制全解
TaskCompletionSource (TCS)
一個(gè)可以手動(dòng)控制狀態(tài)的 Task。它允許你創(chuàng)建一個(gè)任務(wù),并在稍后的某個(gè)時(shí)間點(diǎn)手動(dòng)將其標(biāo)記為“已完成”。
Task.WhenAny
超時(shí)處理機(jī)制。通過(guò)將“結(jié)果任務(wù)”和“延遲任務(wù)(Task.Delay)”放在一起競(jìng)爭(zhēng),確保程序不會(huì)因?yàn)橄G失而永久死鎖。
private TaskCompletionSource<string> _transTaskSource;
private TaskCompletionSource<bool> _transAviResultSource;
public bool TransResult{get;set;}
public string ImageFileName{get;set;}
private void OnAppMessageReceived(AppMessageEventArgs obj)
{
if (obj.MessageId == "xxx")
{
Dictionary<string, object> dic = JsonHelper.Deserialize<Dictionary<string, object>>(obj.Value);
if (dic["FileName"].ToString().Contains("avi"))
{
bool result = dic["Result"].ToString() == "1";
_transAviResultSource?.TrySetResult(result);
}
else
{
string fileName = dic["FileName"].ToString();
_transTaskSource?.TrySetResult(fileName);
}
}
}
protected virtual async Task<string> OnTransStandard(string fileFormat)
{
switch (fileFormat)
{
case "avi":
_transAviResultSource = new TaskCompletionSource<bool>();
var result = _transService.Trans2Avi();
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(15));
var completedTask = await Task.WhenAny(_transAviResultSource.Task, timeoutTask);
if (completedTask == _transAviResultSource.Task)
{
TransResult = result;
}
break;
default:
_trasnTaskSource = new TaskCompletionSource<string>();
_transService.Trans();
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
var completedTask = await Task.WhenAny(_trasnTaskSource.Task, timeoutTask);
if (completedTask == _trasnTaskSource.Task)
{
ImageFileName = await _trasnTaskSource.Task;
}
else
{
// TODO
}
break;
}
}優(yōu)化版本
using System.Collections.Concurrent;
// 使用字典支持并發(fā),Key 為唯一標(biāo)識(shí)(如文件名或請(qǐng)求ID)
private readonly ConcurrentDictionary<string, TaskCompletionSource<string>> _transTasks = new();
private readonly ConcurrentDictionary<string, TaskCompletionSource<bool>> _aviTasks = new();
private void OnAppMessageReceived(AppMessageEventArgs obj)
{
if (obj.MessageId != "xxx") return;
var dic = JsonHelper.Deserialize<Dictionary<string, object>>(obj.Value);
string fileName = dic["FileName"]?.ToString() ?? string.Empty;
if (fileName.Contains("avi"))
{
bool result = dic["Result"]?.ToString() == "1";
// 嘗試從字典中移除并設(shè)置結(jié)果,確保只處理一次
if (_aviTasks.TryRemove(fileName, out var tcs))
{
tcs.TrySetResult(result);
}
}
else
{
if (_transTasks.TryRemove(fileName, out var tcs))
{
tcs.TrySetResult(fileName);
}
}
}
protected virtual async Task<string> OnTransStandard(string fileFormat, string fileName)
{
// 1. 設(shè)置超時(shí)取消令牌
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(fileFormat == "avi" ? 15 : 30));
try
{
if (fileFormat == "avi")
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_aviTasks[fileName] = tcs;
_transService.Trans2Avi();
// 使用 Register 綁定取消令牌到 TCS
using (cts.Token.Register(() => tcs.TrySetCanceled()))
{
TransResult = await tcs.Task;
return fileName;
}
}
else
{
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
_transTasks[fileName] = tcs;
_transService.Trans();
using (cts.Token.Register(() => tcs.TrySetCanceled()))
{
ImageFileName = await tcs.Task;
return ImageFileName;
}
}
}
catch (OperationCanceledException)
{
// 統(tǒng)一處理超時(shí)邏輯
HandleTimeout(fileName, fileFormat);
return string.Empty;
}
finally
{
// 確保清理字典,防止內(nèi)存泄漏
_aviTasks.TryRemove(fileName, out _);
_transTasks.TryRemove(fileName, out _);
}
}同步阻塞式的等待機(jī)制(同步原語(yǔ))
傳統(tǒng)的線程同步對(duì)象 ManualResetEvent 來(lái)強(qiáng)制當(dāng)前線程“停下”執(zhí)行,直到收到特定的信號(hào)。
調(diào)用 OnDataTransformation 的線程會(huì)被物理掛起,不消耗 CPU 周期,但會(huì)占用一個(gè)線程資源,直到 _event.Set() 被調(diào)用。
| 特性 | ManualResetEvent (手動(dòng)) | AutoResetEvent (自動(dòng)) |
|---|---|---|
| 放行數(shù)量 | 調(diào) Set() 后,所有正在等待的線程都會(huì)被放行。 | 調(diào)用 Set() 后,僅有一個(gè)線程會(huì)被放行。 |
| 復(fù)位方式 | 必須手動(dòng)調(diào)用 Reset() 才會(huì)再次阻塞線程。 | 只要有一個(gè)線程通過(guò),它會(huì)自動(dòng)回到阻塞狀態(tài)。 |
| 典型場(chǎng)景 | 廣播/通知:一個(gè)信號(hào)通知多個(gè)任務(wù)同時(shí)開(kāi)始。 | 同步/排隊(duì):確保對(duì)某個(gè)資源的訪問(wèn)是串行的。 |
public bool TransResult { get; set; }
public string Filenames { get; set; }
private static readonly ManualResetEvent _event = new ManualResetEvent(false);
private void OnAppMessageReceived(AppMessageEventArgs obj)
{
if (obj.MessageId == "xxx")
{
Dictionary<string, object> dic = JsonHelper.Deserialize<Dictionary<string, object>>(obj.Value);
if (dic["FileName"].ToString().Contains("avi"))
{
TransResult = dic["Result"].ToString() == "1";
_event.Set();
}
else
{
Filenames = dic["FileName"].ToString();
_event.Set();
}
}
}
public virtual void OnDataTransformation()
{
cancellationToken.ThrowIfCancellationRequested();
_TranService.TransImage();
_event.Reset();
_event.WaitOne();
cancellationToken.ThrowIfCancellationRequested();
}選擇 ManualResetEvent 還是 AutoResetEvent 取決于你的控制目標(biāo):是想給“一群人”發(fā)信號(hào),還是想讓“一個(gè)人”過(guò)閘機(jī)。
1. ManualResetEvent(手動(dòng)重置)
適用場(chǎng)景:狀態(tài)通知 / 廣播 (Broadcasting)
它通常用于表示一個(gè)“開(kāi)關(guān)”或“階段完成”的狀態(tài)。一旦這個(gè)狀態(tài)達(dá)成了,所有依賴(lài)它的線程都可以繼續(xù)。
典型例子:系統(tǒng)初始化
- 你的主程序啟動(dòng)時(shí)有多個(gè)后臺(tái)服務(wù)(數(shù)據(jù)庫(kù)連接、緩存加載、配置文件讀?。?ul>
- 主線程調(diào)用
_initEvent.WaitOne()。 - 只有當(dāng)所有初始化工作全部完成后,才調(diào)用一次
Set()。 - 此時(shí),所有卡在
WaitOne處的邏輯都會(huì)同時(shí)被釋放。
典型例子:暫停/恢復(fù)功能
- 在下載器或播放器中,按下“暫停”即
Reset()(關(guān)閘),所有下載線程WaitOne;按下“開(kāi)始”即Set()(開(kāi)閘),所有線程同時(shí)恢復(fù)工作。
2. AutoResetEvent(自動(dòng)重置)
適用場(chǎng)景:獨(dú)占資源 / 生產(chǎn)者-消費(fèi)者 (Queueing)
它通常用于確保任務(wù)的串行化執(zhí)行,或者作為簡(jiǎn)單的線程間信號(hào)傳遞。
典型例子:任務(wù)隊(duì)列(單線程處理)
- 你有一個(gè)后臺(tái)線程專(zhuān)門(mén)處理發(fā)郵件的操作。
- 當(dāng)有新郵件進(jìn)入隊(duì)列時(shí),調(diào)用一次
Set()。 - 后臺(tái)處理線程
WaitOne()收到信號(hào),起來(lái)發(fā)一封郵件。 - 發(fā)完后,由于是
AutoReset,它會(huì)自動(dòng)回到阻塞狀態(tài),等待下一次Set()。
- 當(dāng)有新郵件進(jìn)入隊(duì)列時(shí),調(diào)用一次
典型例子:由于硬件限制的互斥訪問(wèn)
- 像你代碼中這種“發(fā)送指令 -> 等待硬件回傳”的模式。如果你想確保發(fā)一個(gè)收一個(gè),且不希望第二個(gè)指令在第一個(gè)指令沒(méi)返回前就跑掉,
AutoResetEvent更安全,因?yàn)樗幚硗暌淮螘?huì)自動(dòng)“關(guān)門(mén)”。
3. 對(duì)比
| 維度 | ManualResetEvent | AutoResetEvent |
|---|---|---|
| 形象比喻 | 大門(mén)的閘刀開(kāi)關(guān):拉上去,所有人都能進(jìn);拉下來(lái),所有人都得停。 | 地鐵的旋轉(zhuǎn)閘機(jī):刷一次卡(Set),只能進(jìn)去一個(gè)人,進(jìn)去后閘機(jī)立刻鎖死。 |
| 核心邏輯 | 狀態(tài)驅(qū)動(dòng):側(cè)重于“某個(gè)條件是否達(dá)成”。 | 事件驅(qū)動(dòng):側(cè)重于“某個(gè)動(dòng)作是否發(fā)生”。 |
| 重置時(shí)機(jī) | 你認(rèn)為這個(gè)狀態(tài)不再有效時(shí)(手動(dòng))。 | 線程穿過(guò) WaitOne 的那一刻(自動(dòng))。 |
4. 為什么用得少了?
在高性能開(kāi)發(fā)中,這兩個(gè)類(lèi)正逐漸被以下方案取代:
- TaskCompletionSource (TCS):
- 理由:它是異步非阻塞的(Async/Non-blocking)。前面的
Manual/Auto都會(huì)死死占住一個(gè)操作系統(tǒng)線程,非常浪費(fèi)資源。
- 理由:它是異步非阻塞的(Async/Non-blocking)。前面的
- SemaphoreSlim (信號(hào)量):
- 理由:它比
AutoResetEvent更強(qiáng)大。它支持WaitAsync(異步等待),而且可以控制允許 N 個(gè)線程同時(shí)通過(guò),而不僅僅是一個(gè)。
- 理由:它比
- ManualResetEventSlim:
- 理由:如果你非要用同步等待,請(qǐng)優(yōu)先使用帶
Slim后綴的版本。它在等待時(shí)間很短時(shí)會(huì)先進(jìn)行“自旋(Spin)”,不直接切換到昂貴的內(nèi)核模式,性能更好。
- 理由:如果你非要用同步等待,請(qǐng)優(yōu)先使用帶
異步等待 (Await) vs 同步掛起 (Wait)
| 維度 | TaskCompletionSource | Manual/AutoResetEvent |
|---|---|---|
| 編程模型 | 異步 (Asynchronous)?;?Task,符合現(xiàn)代 .NET 開(kāi)發(fā)習(xí)慣。 | 同步 (Synchronous)。基于操作系統(tǒng)的內(nèi)核對(duì)象。 |
| 線程利用率 | 高。await 時(shí)線程會(huì)釋放回線程池,去處理其他任務(wù)。 | 低。當(dāng)前線程被徹底卡死(Block),什么都干不了。 |
| 超時(shí)處理 | 靈活。通過(guò) Task.Delay 或 CancellationToken 輕松實(shí)現(xiàn)。 | 較硬。需給 WaitOne(timeout) 傳參,且寫(xiě)法略顯臃腫。 |
| UI 響應(yīng)性 | 友好。如果在 UI 線程調(diào)用,界面不會(huì)卡死。 | 危險(xiǎn)。如果在 UI 線程調(diào)用,界面會(huì)直接崩潰/無(wú)響應(yīng)(Deadlock)。 |
| 并發(fā)支持 | 較好。通過(guò) tcs 實(shí)例可以區(qū)分不同的請(qǐng)求。 | 差。static 的 _event 意味著全局只能同時(shí)處理一個(gè)任務(wù)。 |
第一種 (TaskCompletionSource) —— 手機(jī)短信提醒: 你該干嘛干嘛(去洗澡、打游戲),線程被釋放回去了??爝f到了,手機(jī)響了(Task 完成),你再回到門(mén)口處理快遞。這種是非阻塞的。
第二種 (ManualResetEvent) —— 站在門(mén)口死等: 你推掉了一切活動(dòng),就站在門(mén)口盯著路口(線程被掛起)??爝f員沒(méi)來(lái),你哪也不去,也不說(shuō)話(huà)??爝f員一招手(Set),你立刻動(dòng)起來(lái)。這種是同步阻塞的。
同步方式 (EventWaitHandle)
// 這是一條死胡同,除非有人開(kāi)門(mén),否則車(chē)(線程)就停死在這里 _event.WaitOne(); // 只有開(kāi)門(mén)后,才能跑這一行 DoNextStep();
- 后果:如果你在 UI 線程(如點(diǎn)擊按鈕)里這么寫(xiě),你的軟件界面會(huì)直接“未響應(yīng)”,因?yàn)?UI 線程被阻塞了,沒(méi)法處理鼠標(biāo)點(diǎn)擊和界面刷新。
異步方式 (TaskCompletionSource)
// 這是一條路口,車(chē)(線程)發(fā)現(xiàn)紅燈,就先掉頭去干別的活了 await _tcs.Task; // 綠燈亮了(SetResult),會(huì)有另一輛車(chē)(或原車(chē))回來(lái)繼續(xù)跑 DoNextStep();
- 后果:UI 依然絲滑。
await釋放了當(dāng)前線程,讓它回消息循環(huán)里去處理界面繪制,等結(jié)果到了再回來(lái)。
.NET中的位置
在 .NET 專(zhuān)門(mén)的同步分類(lèi)中,它們屬于內(nèi)核模式同步對(duì)象(Kernel-mode objects)。
| 類(lèi)別 | 代表組件 | 特點(diǎn) |
|---|---|---|
| 內(nèi)核模式 (同步) | ManualResetEvent, AutoResetEvent, Mutex | 重型。涉及操作系統(tǒng)內(nèi)核切換,跨進(jìn)程可用,但性能開(kāi)銷(xiāo)大。 |
| 混合模式 (同步) | ManualResetEventSlim, SemaphoreSlim | 輕量。先自旋再掛起,性能極高,是現(xiàn)代同步的首選。 |
| 異步模式 | TaskCompletionSource, Task.WhenAny | 現(xiàn)代。完全不阻塞線程,支撐高并發(fā)的核心。 |
到此這篇關(guān)于C# 異步回調(diào)與等待機(jī)制全解的文章就介紹到這了,更多相關(guān)C# 異步回調(diào)與等待機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#實(shí)現(xiàn)的SN快速輸入工具實(shí)例
這篇文章主要介紹了C#實(shí)現(xiàn)的SN快速輸入工具,以實(shí)例的形式詳細(xì)講述了C#實(shí)現(xiàn)序列號(hào)快速輸入的方法,是非常實(shí)用的技巧,需要的朋友可以參考下2014-11-11
C#使用Oracle.ManagedDataAccess.dll組件連接Oracle數(shù)據(jù)庫(kù)
這篇文章介紹了C#使用Oracle.ManagedDataAccess.dll組件連接Oracle數(shù)據(jù)庫(kù)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05
.NET WinForm實(shí)現(xiàn)在listview中添加progressbar的方法
這篇文章主要介紹了.NET WinForm實(shí)現(xiàn)在listview中添加progressbar的方法,結(jié)合實(shí)例形式簡(jiǎn)單分析了進(jìn)度條控件的添加與使用方法,需要的朋友可以參考下2017-05-05
C#RSA對(duì)接JAVA中RSA方式代碼實(shí)例
在本篇文章中小編給各位整理的是關(guān)于C#RSA對(duì)接JAVA中RSA方式代碼實(shí)例,有需要的朋友們可以參考一下。2019-09-09
WPF實(shí)現(xiàn)自定義Panel面板的示例詳解
WPF中的Panel(面板),是繼承自FrameworkElement的抽象類(lèi),表示一個(gè)可以用來(lái)排列子元素的面板,本文主要來(lái)和大家聊聊WPF如何實(shí)現(xiàn)自定義Panel,感興趣的可以了解下2023-09-09
C#實(shí)現(xiàn)鬧鐘AlarmClock實(shí)例代碼
這篇文章主要介紹了C#實(shí)現(xiàn)鬧鐘AlarmClock實(shí)例代碼,很實(shí)用的功能,需要的朋友可以參考下2014-08-08
Silverlight實(shí)現(xiàn)星星閃爍動(dòng)畫(huà)
這篇文章主要為大家詳細(xì)介紹了Silverlight實(shí)現(xiàn)星星閃爍動(dòng)畫(huà),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07
C# Color.FromArgb()及系統(tǒng)顏色對(duì)照表一覽
這篇文章主要介紹了C# Color.FromArgb()及系統(tǒng)顏色對(duì)照表一覽,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-01-01

