阻塞(Blocking)
阻塞
- 如果線程的執(zhí)行由于某種原因?qū)е聲和#敲淳驼J(rèn)為該線程被阻塞了京腥。
- 例如在 Sleep 或者 Join 等待其他線程結(jié)束。
- 被阻塞的線程會(huì)立即將其處理器的時(shí)間片生成給其它線程辑鲤,從此就不再消耗處理器時(shí)間逻悠,直到滿足其阻塞條件為止猴仑。
解除阻塞(Unblocking)
- 當(dāng)遇到下列四種情況的時(shí)候棒口,就會(huì)解除阻塞:
- 阻塞條件被滿足
- 操作超時(shí)(如果設(shè)置超時(shí)的話)
- 通過(guò) Thread.Interrupt() 進(jìn)行打斷
- 通過(guò) Thread.Abort() 進(jìn)行中止
上下文切換(Context Switching)
當(dāng)線程阻塞或解除阻塞時(shí)瓤鼻,操作系統(tǒng)將執(zhí)行上下文切換保屯。這會(huì)產(chǎn)生少量開(kāi)銷手负,通常為 1 或者 2 微秒。
I/O-bound vs Compute-bound(或CPU-Bound)
- 一個(gè)花費(fèi)大部分時(shí)間等待某事發(fā)生的操作稱為 I/O-bound
- I/O 綁定操作通常涉及輸入或輸出姑尺,但這不是硬性要求:Thread.Sleep() 也被視為 I/O-bound
- 相反竟终,一個(gè)花費(fèi)大部分時(shí)間執(zhí)行 CPU 密集型工作的操作稱為 Compute-bound。
阻塞 vs 忙等待(自旋 Spinning)
- I/O-bound 操作的工作方式有兩種:
- 在當(dāng)前線程上同步地等待切蟋,Console.ReadLine(), Thread.Sleep(), Thread.Join()...
- 異步地操作统捶,在稍后操作完成時(shí)觸發(fā)一個(gè)回調(diào)動(dòng)作。
- 同步等待的 I/O-bound 操作將大部分時(shí)間花在阻塞線程上柄粹。
- 它們也可以周期性地在一個(gè)循環(huán)里進(jìn)行“打轉(zhuǎn)(自旋)”
while (DateTime.Now < nextStartTime)
Thread.Sleep(100); //這個(gè)算半阻塞半自旋
while (DateTime.Now < nextStartTime); //這個(gè)是純自旋
阻塞和忙等待的細(xì)微差別:
- 首先喘鸟,如果您希望條件很快得到滿足(幾微秒內(nèi)),則短暫自旋可能會(huì)很有效驻右,因?yàn)樗苊饬松舷挛那袚Q的開(kāi)銷和延遲什黑。
- .NET 提供了特殊的方法和類,來(lái)提供幫助 SpinLock 和 SpinWait
- 其次堪夭,阻塞也不是零成本愕把。這是因?yàn)槊總€(gè)線程在生存期間會(huì)占用大約1MB的內(nèi)存,并會(huì)給 CLR 和操作系統(tǒng)帶來(lái)持續(xù)的管理開(kāi)銷森爽。
- 因此恨豁,在需要處理成百上千個(gè)并發(fā)操作的大量 I/O-bound 程序的上下文中,阻塞可能會(huì)很麻煩
- 所以爬迟,此類程序需要使用基于回調(diào)的方法橘蜜,在等待時(shí)完全撤銷其線程。
前臺(tái)和后臺(tái)線程(Foreground vs Background Threads)
- 可以通過(guò) IsBackground 屬性判斷線程是否后臺(tái)線程
- 進(jìn)程以這種形式終止的時(shí)候付呕,后臺(tái)線程執(zhí)行棧中的 finally 塊就不會(huì)執(zhí)行了计福。(少數(shù)幾個(gè) finally 不執(zhí)行的情況)
- 如果想讓它執(zhí)行捧请,可以在退出程序時(shí)使用 Join 來(lái)等待后臺(tái)線程(自建線程),或者使用 signal construct(線程池)
- 應(yīng)用程序無(wú)法正常退出的一個(gè)常見(jiàn)原因是有前臺(tái)線程棒搜。
信號(hào)(Signaling)
- 有時(shí)疹蛉,你需要讓某線程一直處于等待的狀態(tài),直至接收到其他線程發(fā)來(lái)的通知力麸。這就叫做 signaling(發(fā)送信號(hào))可款。
- 最簡(jiǎn)單的信號(hào)結(jié)構(gòu)就是 ManualResetEvent。
- 調(diào)用它上面的 WaitOne 方法會(huì)阻塞當(dāng)前的線程克蚂,直到另一個(gè)線程通過(guò)調(diào)用 Set 方法來(lái)開(kāi)啟信號(hào)闺鲸。
- 調(diào)用完 Set 之后,信號(hào)會(huì)處于“打開(kāi)”的狀態(tài)埃叭∶校可以通過(guò)調(diào)用 Reset 方法將其再次關(guān)閉。
示例
class Program
{
static void Main(string[] args)
{
var signal = new ManualResetEvent(false);
new Thread(() =>
{
Console.WriteLine("Waiting for signal...");
signal.WaitOne();
signal.Dispose();
Console.WriteLine("Got signal!");
}).Start();
Thread.Sleep(3000);
signal.Set(); //打開(kāi)了信號(hào)
}
}
富客戶端應(yīng)用程序的線程
在 WPF赤屋,UWP立镶,WinForm 等程序中,通常都有一個(gè)渲染UI类早、監(jiān)聽(tīng)鼠標(biāo)鍵盤等事件的 UI線程
針對(duì)耗時(shí)的操作媚媒,通常都是啟用一個(gè) worker 線程執(zhí)行,執(zhí)行完畢再更新到 UI
-
富客戶端應(yīng)用的線程模型通常是:
- UI 元素和控件只能從創(chuàng)建它們的線程來(lái)進(jìn)行訪問(wèn)(通常是主 UI 線程)
- 當(dāng)想從 worker 線程更新 UI 的時(shí)候涩僻,你必須把請(qǐng)求交給 UI 線程
-
比較底層的實(shí)現(xiàn)是:
- WPF缭召,在元素的 Dispatcher 對(duì)象上調(diào)用 BeginInvoke 或 Invoke
- WinForm,調(diào)用控件的 BeginInvoke 或 Invoke
- UWP逆日,調(diào)用 Dispatcher 對(duì)象上的 RunAsync 或 Invoke
所有這些方法都接受一個(gè)委托嵌巷。
BeginInvoke 或 RunAsync 通過(guò)將委托排隊(duì)到 UI 線程的消息隊(duì)列來(lái)執(zhí)行工作。
-
而 Invoke 執(zhí)行相同的操作室抽,但隨后會(huì)進(jìn)行阻塞搪哪,直到 UI 線程讀取并處理消息。
- 因此狠半,Invoke 允許您從方法中獲取返回值噩死。
- 如果不需要返回值,BeginInvoke/RunAsync 更可取神年,因?yàn)樗鼈儾粫?huì)阻塞調(diào)用方已维,也不會(huì)引入死鎖的可能性
SynchronizationContext 同步上下文
- 在 System.ComponentModel 下有一個(gè)抽象類:SynchronizationContext,它使得 Thread Marshaling 得到泛化已日。
- Marshaling:可以理解為序列化垛耳。Serialize 是 Marshaling 的一種
- 針對(duì)移動(dòng)、桌面等富客戶端應(yīng)用的 API,它們都定義和實(shí)例化了 SynchronizationContext 的子類
- 可以通過(guò)靜態(tài)屬性 SynchronizationContext.Current 來(lái)獲得(在UI線程)
- 捕獲該屬性讓你可以在稍后的時(shí)候從 worker 線程向 UI 線程 發(fā)送數(shù)據(jù)
- 調(diào)用 Post 就相當(dāng)于調(diào)用 Dispatch 或 Control 上面的 BeginInvoke 方法
- 還有一個(gè) Send 方法堂鲜,等價(jià)于 Invoke 方法
ThreadPool 線程池
- 當(dāng)開(kāi)始一個(gè)線程的時(shí)候栈雳,將花費(fèi)幾百微秒來(lái)組織類似以下的內(nèi)容:
- 一個(gè)新的局部變量棧(stack)
- 線程池就可以節(jié)省這種開(kāi)銷:
- 通過(guò)預(yù)先創(chuàng)建一個(gè)可以循環(huán)使用線程的池來(lái)減少這一開(kāi)銷。
- 線程池對(duì)于高效的并行編程和細(xì)粒度并發(fā)是必不可少的
- 它允許在不被線程啟動(dòng)的開(kāi)銷淹沒(méi)的情況下運(yùn)行短期操作
使用線程池線程需要注意的幾點(diǎn)
- 不可以設(shè)置池內(nèi)線程的 Name
- 池線程都是后臺(tái)線程
- 阻塞池線程可使性能降級(jí)
- 你可以自由地更改池線程的優(yōu)先級(jí)缔莲。當(dāng)它釋放回池的時(shí)候哥纫,優(yōu)先級(jí)將還原為正常狀態(tài)
- 可以通過(guò) Thread.CurrentThread.IsThreadPoolThread 屬性來(lái)判斷是否執(zhí)行在池線程上
進(jìn)入線程池
- 最簡(jiǎn)單的、顯式的在池線程運(yùn)行代碼的方式就是使用 Task.Run
誰(shuí)使用了線程池
- WCF痴奏、Remoting蛀骇、ASP.NET、ASMX Web Services 應(yīng)用服務(wù)器
- System.Timers.Timer读拆、System.Threading.Timer
- 并行編程結(jié)構(gòu)
- BackgroundWorker 類(現(xiàn)在很多余)
- 異步委托(現(xiàn)在很多余)
線程池中的整潔
- 線程池提供了另一個(gè)功能擅憔,即確保臨時(shí)超出 Compute-Bound 的工作不會(huì)導(dǎo)致 CPU 超額訂閱
- 超額訂閱:活躍的線程超過(guò) CPU 的核數(shù),操作系統(tǒng)就需要對(duì)線程進(jìn)行時(shí)間切片
- 超額訂閱對(duì)性能影響很大檐晕,時(shí)間切片需要昂貴的 上下文切換暑诸,并且可能使 CPU 緩存失效,而 CPU 緩存對(duì)于現(xiàn)代處理器的性能至關(guān)重要
CLR 的策略
- CLR 通過(guò)對(duì)任務(wù)排隊(duì)并對(duì)其啟動(dòng)進(jìn)行節(jié)流限制來(lái)避免線程池中的超額訂閱辟灰。
- 它首先運(yùn)行盡可能多的并發(fā)任務(wù)(只要還有 CPU 核)个榕,然后通過(guò)爬山算法調(diào)整并發(fā)級(jí)別,并在特定方向上不斷調(diào)整工作負(fù)載伞矩。
- 如果吞吐量提高笛洛,它將繼續(xù)朝同一方向(否則將反轉(zhuǎn))夏志。
- 這確保它始終追隨最佳性能曲線乃坤,即使面對(duì)計(jì)算機(jī)上競(jìng)爭(zhēng)的進(jìn)程活動(dòng)時(shí)也是如此
- 如果下面兩點(diǎn)能夠滿足,那么 CLR 的策略將發(fā)揮出最佳效果:
- 工作項(xiàng)大多是短時(shí)間運(yùn)行的(<250ms沟蔑,或者理想情況下<100ms)湿诊,因此 CLR 有很多機(jī)會(huì)進(jìn)行測(cè)量和調(diào)整。
- 大部分時(shí)間都被阻塞的工作項(xiàng)不會(huì)主宰線程池
如果想充分利用 CPU瘦材,那么保持線程池的“整潔”是 非常重要 的厅须。
異步編程
Thread 的問(wèn)題
- 線程(Thread)是用來(lái)創(chuàng)建并發(fā)(Concurrency)的一種低級(jí)別的工具,它有一些限制食棕,尤其是:
- 雖然開(kāi)始線程的時(shí)候可以方便地傳入數(shù)據(jù)朗和,但是當(dāng) Join 的時(shí)候,很難從線程獲得返回值簿晓。
- 可能需要設(shè)置一些共享字段眶拉。
- 如果操作拋出異常,捕獲和傳播該異常都很麻煩憔儿。
- 無(wú)法告訴線程在結(jié)束時(shí)開(kāi)始做另外的工作忆植,你必須進(jìn)行 Join 操作(在進(jìn)程中阻塞當(dāng)前的線程)
- 雖然開(kāi)始線程的時(shí)候可以方便地傳入數(shù)據(jù)朗和,但是當(dāng) Join 的時(shí)候,很難從線程獲得返回值簿晓。
- 很難使用較小的并發(fā)(concurrent)來(lái)組建大型的并發(fā)。
- 導(dǎo)致了對(duì)手動(dòng)同步的更大依賴以及隨之而來(lái)的問(wèn)題。
Task 類
- Task 類可以很好的解決上述問(wèn)題
- Task 是一個(gè)相對(duì)高級(jí)的抽象:它代表了一個(gè)并發(fā)操作
- 該操作可能由 Thread 支持朝刊,或不由 Thread 支持
- Task 是可組合的(可使用 Continuation 把它們串成鏈)
- Tasks 可以使用線程池來(lái)減少啟動(dòng)延遲
- 使用 TaskCompletionSource耀里,Tasks 可以利用回調(diào)的方式,在等待 I/O綁定操作時(shí)完全避免線程拾氓。
開(kāi)始一個(gè) Task:Task.Run()
- Task 類在 System.Threading.Tasks 命名空間下冯挎。
- 開(kāi)始一個(gè) Task 最簡(jiǎn)單的辦法就是使用 Task.Run(4.0 里是 Task.Factory.StartNew)
- 傳入一個(gè) Action 委托即可
- Task 默認(rèn)使用線程池,也就是后臺(tái)線程:
- 當(dāng)主線程結(jié)束時(shí)咙鞍,你創(chuàng)建的所有 tasks 都會(huì)結(jié)束织堂。
- Task.Run 返回一個(gè) Task 對(duì)象,可以使用它來(lái)監(jiān)視其過(guò)程
- 在 Task.Run 之后奶陈,我們沒(méi)有調(diào)用 Start易阳,因?yàn)樵摲椒▌?chuàng)建的是 “熱”任務(wù)(Hot Task)
- 可以通過(guò) Task 的構(gòu)造函數(shù)創(chuàng)建“冷”任務(wù),但是很少這樣做
- 可以通過(guò) Task 的 Status 屬性來(lái)跟蹤 task 的執(zhí)行狀態(tài)吃粒。
Wait 等待
- 調(diào)用 task 的 Wait 方法會(huì)進(jìn)行阻塞直到操作完成
- 相當(dāng)于調(diào)用 thread 上的 Join 方法
- Wait 也可以讓你指定一個(gè)超時(shí)時(shí)間和一個(gè)取消令牌來(lái)提前結(jié)束等待潦俺。
Long-running tasks
- 默認(rèn)情況下,CLR 在線程池中運(yùn)行 Task徐勃,這非常適合短時(shí)間運(yùn)行的 Compute-Bound 類工作事示。
- 針對(duì)長(zhǎng)時(shí)間運(yùn)行的任務(wù)或者阻塞操作,你可以不采用線程池
- 如果同時(shí)運(yùn)行多個(gè) long-running tasks(尤其是其中有處于阻塞狀態(tài)的)僻肖,那么性能將會(huì)受到很大影響肖爵,這時(shí)有比 TaskCreationOptions.LongRunning 更好的辦法:
- 如果任務(wù)是 I/O-bound,TaskCompletionSource 和異步函數(shù)可以讓你用回調(diào)(Continuations)代替線程來(lái)實(shí)現(xiàn)并發(fā)臀脏。
- 如果任務(wù)是 Compute-bound劝堪,生產(chǎn)者/消費(fèi)者隊(duì)列允許你對(duì)任務(wù)的并發(fā)性進(jìn)行限流,避免把其他線程和進(jìn)程餓死揉稚。
Task 的返回值
- Task 有一個(gè)泛型子類叫做 Task<Result>秒啦,它允許發(fā)出一個(gè)返回值。
- 使用 Func<TResult> 委托或兼容的 Lambda 表達(dá)式來(lái)調(diào)用 Task.Run() 就可以得到 Task<TResult>搀玖。
- 隨后余境,可以通過(guò) Result 屬性來(lái)獲得返回的結(jié)果。
- 如果這個(gè) task 還沒(méi)有完成操作灌诅,訪問(wèn) Result 屬性會(huì)阻塞該線程直到該 task 完成操作芳来。
- Task<TResult> 可以看作是一種所謂的“未來(lái)/許諾”(future/promise),在它里面包裹著一個(gè) Result猜拾,在稍后的時(shí)候就會(huì)變得可用即舌。
Task 的異常
- 與 Thread 不一樣,Task 可以很方便地傳播異常
- 如果你的 task 里面拋出了一個(gè)未處理的異常(故障)关带,那么該異常就會(huì)重新被拋出給:
- 調(diào)用了 wait() 的地方
- 訪問(wèn)了 Task<TResult> 的 Result 屬性的地方侥涵。
- CLR 將異常包裹在 AggregateException 里沼撕,以便在并行編程場(chǎng)景中發(fā)揮很好的作用。
- 無(wú)需重新拋出異常芜飘,通過(guò) Task 的 IsFaulted 和 IsCanceled 屬性务豺,也可以檢測(cè)出 Task 是否發(fā)生了故障:
- 如果兩個(gè)屬性都返回 false,那么沒(méi)有錯(cuò)誤發(fā)生嗦明。
- 如果 IsCanceled 為 true笼沥,那就說(shuō)明一個(gè) OperationCanceledException 為該 Task 拋出了。
異常與“自治”的 Task
- 自治的娶牌,“設(shè)置完就不管了”的 Task奔浅。就是指不通過(guò)調(diào)用 Wait() 方法、Result 屬性或 continuation 進(jìn)行會(huì)合的任務(wù)诗良。
- 針對(duì)自治的 Task汹桦,需要像 Thread 一樣,顯式的處理異常鉴裹,避免發(fā)生“悄無(wú)聲息的故障”舞骆。
- 自治 Task 上未處理的異常稱為未觀察到的異常。
未觀察到的異常
- 可以通過(guò)全局的 TaskScheduler.UnobservedTaskException 來(lái)訂閱未觀察到的異常径荔。
- 關(guān)于什么是“未觀察到的異扯角荩”,有一些細(xì)微的差別:
- 使用超時(shí)進(jìn)行等待的 Task总处,如果在超時(shí)后發(fā)生故障狈惫,那么它將會(huì)產(chǎn)生一個(gè)“未觀察到的異常”鹦马。
- 在 Task 發(fā)生故障后胧谈,如果訪問(wèn) Task 的 Exception 屬性,那么該異常就被認(rèn)為是“已觀察到的”菠红。
Continuation
- 一個(gè) Continuation 會(huì)對(duì) Task 說(shuō):“當(dāng)你結(jié)束的時(shí)候第岖,繼續(xù)再做點(diǎn)其他的事”
- Continuation 通常是通過(guò)回調(diào)的方式實(shí)現(xiàn)的
- 當(dāng)操作一結(jié)束,就開(kāi)始執(zhí)行
static void Main(string[] args)
{
Task<int> primeNumberTask = Task.Run(() =>
Enumerable.Range(2, 3000000).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
var awaiter = primeNumberTask.GetAwaiter();
awaiter.OnCompleted(() =>
{
int result = awaiter.GetResult();
Console.WriteLine(result);
});
Console.ReadLine();
}
- 在 task 上調(diào)用 GetAwaiter 會(huì)返回一個(gè) awaiter 對(duì)象
- 它的 OnCompleted 方法會(huì)告訴之前的 task:“當(dāng)你結(jié)束/發(fā)生故障的時(shí)候要執(zhí)行委托”试溯。
- 可以將 Continuation 附加到已經(jīng)結(jié)束的 task 上面,此時(shí) Continuation 將會(huì)被安排立即執(zhí)行郊酒。
awaiter
- 任何可以暴露下列兩個(gè)方法和一個(gè)屬性的對(duì)象就是 awaiter:
- OnCompleted
- GetResult
- 一個(gè) 叫做 IsCompleted 的 bool 屬性
- 沒(méi)有接口或者父類來(lái)統(tǒng)一這些成員遇绞。
- 其中 OnCompleted 是 INotifyCompletion 的一部分
如果發(fā)生故障
- 如果之前的任務(wù)發(fā)生故障,那么當(dāng) Continuation 代碼調(diào)用 awaiter.GetResult() 時(shí)候燎窘,異常會(huì)被重新拋出摹闽。
- 無(wú)需調(diào)用 GetResult(),我們可以直接訪問(wèn) task 的 Result 屬性褐健。
- 但是調(diào)用 GetResult 的好處是付鹿,如果 task 發(fā)生故障澜汤,那么異常會(huì)被直接地拋出,不是包裹在 AggregateException 里面舵匾,這樣的話 catch 塊就簡(jiǎn)潔很多了俊抵。
非泛型 task
- 針對(duì)非泛型的 task,GetResult() 方法返回值是 void坐梯,它就是用來(lái)重新拋出異常的徽诲。
同步上下文
- 如果同步上下文出現(xiàn)了,那么 OnCompleted 會(huì)自動(dòng)捕獲它吵血,并將 Continuation 提交到這個(gè)上下文中谎替。這一點(diǎn)在富客戶端應(yīng)用中非常有用,因?yàn)樗鼤?huì)把 Continuation 放回到 UI 線程中蹋辅。
- 如果是編寫一個(gè)庫(kù)钱贯,則不希望出現(xiàn)上述行為,因?yàn)殚_(kāi)銷較大的 UI 線程切換應(yīng)該在程序運(yùn)行離開(kāi)庫(kù)的時(shí)候只發(fā)生一次侦另,而不是出現(xiàn)在方法調(diào)用之間喷舀。所以,我們可以使用 ConfigureAwait 方法來(lái)避免這種行為淋肾。
static void Main(string[] args)
{
Task<int> primeNumberTask = Task.Run(() =>
Enumerable.Range(2, 3000000).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
var awaiter = primeNumberTask.ConfigureAwait(false).GetAwaiter();
awaiter.OnCompleted(() =>
{
int result = awaiter.GetResult();
Console.WriteLine(result);
});
Console.ReadLine();
}
- 如果沒(méi)有同步上下文出現(xiàn)硫麻,或者你使用的是 ConfigureAwait(false),那么 Continuation 會(huì)運(yùn)行在先前 task 的同一個(gè)線程上樊卓,從而避免不必要的開(kāi)銷拿愧。
ContinueWith
- 另外一種附加 Continuation 的方式就是調(diào)用 task 的 ContinueWith 方法
- ContinueWith 本身返回一個(gè) Task,可以用來(lái)附加更多的 Continuation碌尔。
- 但是浇辜,必須直接處理 AggregateException:
- 如果 task 發(fā)生故障,需要寫額外的代碼來(lái)把 Continuation 封裝(marshall)到 UI 應(yīng)用上唾戚。
- 在非 UI 上下文中柳洋,若想讓 Continuation 和 task 執(zhí)行在同一個(gè)線程上,必須指定 TaskContinuationOptions.ExecuteSynchronously叹坦,否則它將彈回到線程池熊镣。
- ContinueWith 對(duì)于并行編程來(lái)說(shuō)非常有用。
TaskCompletionSource
- Task.Run 創(chuàng)建 Task
- 另一種方式就是用 TaskCompletionSource 來(lái)創(chuàng)建 Task
- TaskCompletionSource 讓你在稍后開(kāi)始和結(jié)束的任意操作中創(chuàng)建 Task
- 它會(huì)為你提供一個(gè)可手動(dòng)執(zhí)行的“從屬”Task
- 指示操作何時(shí)結(jié)束或發(fā)生故障
- 它會(huì)為你提供一個(gè)可手動(dòng)執(zhí)行的“從屬”Task
- 它對(duì) I/O-bound 類工作比較理想
- 可以獲得所有 Task 的好處(傳播值募书、異常绪囱、Continuation等)
- 不需要在操作時(shí)阻塞線程
使用 TaskCompletionSource
- 初始化一個(gè)實(shí)例即可
- 它有一個(gè) Task 屬性可返回一個(gè) Task
- 該 Task 完全由 TaskCompletionSource 控制
- 調(diào)用任意一個(gè)方法都會(huì)給 Task 發(fā)信號(hào):
- 完成、故障莹捡、取消
- 這些方法只能調(diào)用一次鬼吵,如果再次調(diào)用:
- SetXxx 會(huì)拋出異常
- TryXxx 會(huì)返回 false
static void Main(string[] args)
{
var tcs = new TaskCompletionSource<int>();
new Thread(() =>
{
Thread.Sleep(5000); tcs.SetResult(42);
})
{
IsBackground = true
}.Start();
Task<int> task = tcs.Task;
Console.WriteLine(task.Result);
}
下面是利用 TaskCompletionSource,自己實(shí)現(xiàn) Task.Run
Task<TResult> Run<TResult>(Func<TResult> function)
{
var tcs = new TaskCompletionSource<TResult>();
new Thread(() =>
{
try
{
tcs.SetResult(function());
}
catch (System.Exception ex)
{
tcs.SetException(ex);
}
}).Start();
return tcs.Task;
}
static void Main(string[] args)
{
Task<int> task = Run(() =>
{
Thread.Sleep(5000);
return 42;
});
}
TaskCompletionSource 的真正魔力
- 它創(chuàng)建 Task篮赢,但并不占用線程
static void Main(string[] args)
{
var awaiter = GetAnswerToLife().GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine(awaiter.GetResult());
});
}
static Task<int> GetAnswerToLife()
{
var tcs = new TaskCompletionSource<int>();
var timer = new System.Timers.Timer(5000) { AutoReset = false };
timer.Elapsed += delegate { timer.Dispose(); tcs.SetResult(42); };
timer.Start();
return tcs.Task;
}
-P17
同步 vs 異步
- 同步操作會(huì)在返回調(diào)用者之前完成它的工作
- 異步操作會(huì)在返回調(diào)用者之后去做它的(大部分)工作
- 異步的方法更為少見(jiàn)齿椅,會(huì)啟用并發(fā)琉挖,因?yàn)樗墓ぷ鲿?huì)與調(diào)用者并行執(zhí)行
- 異步方法通常會(huì)很快(立即)就會(huì)返回到調(diào)用者,所以叫非阻塞方法
- 目前見(jiàn)到的大部分的異步方法都是通用目的的:
- Thread.Start
- Task.Run
- 可以將 continuation 附加到 Task 的方法
什么是異步編程
- 異步編程的原則是將長(zhǎng)時(shí)間運(yùn)行的函數(shù)寫成異步的涣脚。
- 傳統(tǒng)的做法是將長(zhǎng)時(shí)間運(yùn)行的函數(shù)寫成同步的示辈,然后從新的線程或 Task 進(jìn)行調(diào)用,從而按需引入并發(fā)涩澡。
- 上述異步方式的不同之處在于顽耳,它是從長(zhǎng)時(shí)間運(yùn)行函數(shù)的內(nèi)部啟動(dòng)并發(fā)。這有兩點(diǎn)好處:
- I/O-bound 并發(fā)可不使用線程來(lái)實(shí)現(xiàn)妙同∩涓唬可提高可擴(kuò)展性和執(zhí)行效率;
- 富客戶端在 worker 線程會(huì)使用更少的代碼粥帚,簡(jiǎn)化了線程安全性胰耗。
異步編程的兩種用途
- 編寫高效處理大量并發(fā) I/O 的應(yīng)用程序(典型:服務(wù)器端應(yīng)用)
- 挑戰(zhàn)并不是線程安全(因?yàn)楣蚕頎顟B(tài)通常是最小化的),而是執(zhí)行效率
- 特別的芒涡,每個(gè)網(wǎng)絡(luò)請(qǐng)求并不會(huì)消耗一個(gè)線程
- 挑戰(zhàn)并不是線程安全(因?yàn)楣蚕頎顟B(tài)通常是最小化的),而是執(zhí)行效率
- 富客戶端應(yīng)用程序 調(diào)用圖(call graph)
- 在富客戶端應(yīng)用里簡(jiǎn)化線程安全柴灯。
- 如果調(diào)用圖中任何一個(gè)操作是長(zhǎng)時(shí)間運(yùn)行的,那么整個(gè) call graph 必須運(yùn)行在 worker 線程上费尽,以保證 UI 響應(yīng)赠群。
- 得到一個(gè)橫跨多個(gè)方法的單一并發(fā)操作(粗粒度)
- 需要為 call graph 中的每個(gè)方法考慮線程安全。
- 異步的 call graph旱幼,直到需要才開(kāi)啟一個(gè)線程查描,通常較淺(I/O-bound 操作完全不需要)
- 其它的方法可以在 UI 線程執(zhí)行,線程安全簡(jiǎn)化柏卤。
- 并發(fā)的粒度適中:
- 一連串小的并發(fā)操作冬三,操作之間會(huì)彈回到 UI 線程
- 如果調(diào)用圖中任何一個(gè)操作是長(zhǎng)時(shí)間運(yùn)行的,那么整個(gè) call graph 必須運(yùn)行在 worker 線程上费尽,以保證 UI 響應(yīng)赠群。
經(jīng)驗(yàn)之談
- 為了獲得上述好處,下列操作建議異步編寫:
- I/O-bound 和 Compute-bound 操作
- 執(zhí)行超過(guò) 50 毫秒的操作
- 另一方面過(guò)細(xì)的粒度會(huì)損害性能缘缚,因?yàn)楫惒讲僮饕灿虚_(kāi)銷勾笆。
-P18
異步編程和 Continuation
- Task 非常適合異步編程,因?yàn)樗鼈冎С?Continuation(對(duì)異步非常重要)
- 第 16 講里面 TaskCompletionSource 的例子桥滨。
- TaskCompletionSource 是實(shí)現(xiàn)底層 I/O-bound 異步方法的一種標(biāo)準(zhǔn)方式
- 對(duì)于 Compute-bound 方法窝爪,Task.Run 會(huì)初始化綁定線程的并發(fā)。
- 把 task 返回調(diào)用者该园,創(chuàng)建異步方法酸舍;
- 異步編程的區(qū)別:目標(biāo)是在調(diào)用圖較低的位置來(lái)這樣做。
- 富客戶端應(yīng)用中里初,高級(jí)方法可以保留在UI線程和訪問(wèn)控制以及共享狀態(tài)上,不會(huì)出現(xiàn)線程安全問(wèn)題忽舟。
例子1:粗粒度的異步
static void Main(string[] args)
{
Task.Run(() => DisplayPrimeCounts());
Console.ReadKey();
}
static void DisplayPrimeCounts()
{
for (int i = 0; i < 10; i++)
Console.WriteLine(GetPrimesCount(i * 1000000 + 2, 1000000) +
" primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
Console.WriteLine("Done!");
}
static int GetPrimesCount(int start, int count)
{
return ParallelEnumerable.Range(start, count).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0));
}
例子2:改善后的異步
static void Main(string[] args)
{
DisplayPrimeCounts();
Console.ReadKey();
}
static void DisplayPrimeCounts()
{
for (int i = 0; i < 10; i++)
{
var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
awaiter.OnCompleted(() =>
Console.WriteLine(awaiter.GetResult() + " primes between... "));
}
Console.WriteLine("Done!");
}
static Task<int> GetPrimesCountAsync(int start, int count)
{
return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}
看執(zhí)行結(jié)果双妨,可以發(fā)現(xiàn)雖然實(shí)現(xiàn)了異步淮阐,但是順序是亂的。
語(yǔ)言對(duì)異步的支持非常重要
- 需要對(duì) task 的執(zhí)行序列化刁品。
- 例如 Task B 依賴于 Task A 的執(zhí)行結(jié)果泣特。
- 為此,必須在 continuation 內(nèi)部觸發(fā)下一次循環(huán)
例子3:順序化執(zhí)行
static void Main(string[] args)
{
DisplayPrimeCounts();
Console.ReadKey();
}
static void DisplayPrimeCounts()
{
DisplayPrimeCountsFrom(0);
}
static void DisplayPrimeCountsFrom(int i)
{
var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine(awaiter.GetResult() + " primes between... ");
if (++i < 10)
{
DisplayPrimeCountsFrom(i);
}
else Console.WriteLine("Done");
});
}
static Task<int> GetPrimesCountAsync(int start, int count)
{
return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}
(例子4太麻煩就不寫了)
async 與 await
對(duì)于不想復(fù)雜地實(shí)現(xiàn)異步非常重要挑随。
例子5
static async Task Main(string[] args)
{
await DisplayPrimeCountsAsync();
}
static async Task DisplayPrimeCountsAsync()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000) +
" primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
}
Console.WriteLine("Done!");
}
static Task<int> GetPrimesCountAsync(int start, int count)
{
return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}
- 命令式循環(huán)結(jié)構(gòu)不要和 continuation 混合在一起状您,因?yàn)樗鼈円蕾囉诋?dāng)前本地狀態(tài)。
- 另一種實(shí)現(xiàn)兜挨,函數(shù)式寫法(Linq 查詢)膏孟,它也是 響應(yīng)式編程(Rx)的基礎(chǔ)。
-P19
await
- async 和 await 關(guān)鍵字可以讓你寫出和同步代碼一樣簡(jiǎn)潔且結(jié)構(gòu)相同的異步代碼
awaiting
- await 關(guān)鍵字簡(jiǎn)化了附加 continuation 的過(guò)程拌汇。
var result = await expression;
statement(s);
就相當(dāng)于:
var awaiter = expression.GetAwaiter();
awaiter.OnCompleted(() =>
{
var result = awaiter.GetResult();
statement(s);
});
async 修飾符
- async 修飾符會(huì)讓編譯器把 await 當(dāng)作關(guān)鍵字而不是標(biāo)識(shí)符(C# 5 以前可能會(huì)使用 await 作為標(biāo)識(shí)符)
- async 修飾符只能應(yīng)用于方法(包括 lambda 表達(dá)式)
- 該方法可以返回 void柒桑、Task、Task<TResult>
- async 修飾符對(duì)方法的簽名或 public 元數(shù)據(jù)沒(méi)有影響(和 unsafe 一樣)噪舀,它只會(huì)影響方法內(nèi)部魁淳。
- 在接口內(nèi)使用 async 是沒(méi)有意義的
- 使用 async 來(lái)重載非 async 的方法卻是合法的
- 使用了 async 修飾符的方法就是“異步函數(shù)”。
異步方法如何執(zhí)行
- 遇到 await 表達(dá)式与倡,執(zhí)行(正常情況下)會(huì)返回調(diào)用者
- 就像 iterator 里面的 yield return界逛。
- 在返回前,運(yùn)行時(shí)會(huì)附加一個(gè) continuation 到 await 的 task
- 為保證 task 結(jié)束時(shí)纺座,執(zhí)行會(huì)跳回原方法息拜,從停止的地方繼續(xù)執(zhí)行。
- 如果發(fā)生故障比驻,那么異常會(huì)被重新拋出
- 如果一切正常该溯,那么它的返回值就會(huì)賦給 await 表達(dá)式
可以 await 什么?
- 你 await 的表達(dá)式通常是一個(gè) task
- 也可以滿足下列條件的任意對(duì)象:
- 有 GetAwaiter 方法别惦,它返回一個(gè) awaiter(實(shí)現(xiàn)了 INotifyCompletion.OnCompleted 接口)
- 返回適當(dāng)類型的 GetResult 方法
- 一個(gè) bool 類型的 IsCompleted 屬性
捕獲本地狀態(tài)
- await 表達(dá)式的最牛之處就是它幾乎可以出現(xiàn)在任何地方狈茉。
- 特別的,在異步方法內(nèi)掸掸,await 表達(dá)式可以替換任何表達(dá)式氯庆。
- 除了 lock 表達(dá)式和 unsafe 上下文
await 之后在哪個(gè)線程上執(zhí)行
- 在 await 表達(dá)式之后,編譯器依賴于 continuation(通過(guò) awaiter 模式)來(lái)繼續(xù)執(zhí)行
- 如果在富客戶端應(yīng)用的 UI 線程上扰付,同步上下文會(huì)保證后續(xù)是在原線程上執(zhí)行堤撵;
- 否則,就會(huì)在 task 結(jié)束的線程上繼續(xù)執(zhí)行羽莺。
UI 上的 await
- 本例中实昨,只有 GetPrimesCountAsync 中的代碼在 worker 線程上運(yùn)行
- Go 中的代碼會(huì)“租用” UI 線程上的時(shí)間
- 可以說(shuō):Go是在消息循環(huán)中“偽并發(fā)”地執(zhí)行
- 也就是說(shuō):它和 UI 線程處理的其他事件是穿插執(zhí)行的
- 因?yàn)檫@種偽并發(fā),唯一能發(fā)生“搶占”的時(shí)刻就是在 await 期間盐固。
- 這其實(shí)簡(jiǎn)化了線程安全荒给,防止重新進(jìn)入即可
- 這種并發(fā)發(fā)生在調(diào)用棧較淺的地方(Task.Run 調(diào)用的代碼里)
- 為了從該模型獲益丈挟,真正的并發(fā)代碼要避免訪問(wèn)共享狀態(tài)或 UI 控件。
與粗粒度的并發(fā)相比
- 例如使用 BackgroundWorker(例子志电,Task.Run)
- 整個(gè)同步調(diào)用圖都在 worker 線程上
- 必須在代碼中到處使用 Dispatcher.BeginInvoke
- 循環(huán)本身在 worker 線程上
- 引入了 race condition
- 若實(shí)現(xiàn)取消和過(guò)程報(bào)告曙咽,會(huì)使得線程安全問(wèn)題更容易發(fā)生,在方法中新添加任何的代碼也是同樣的效果挑辆。
編寫異步函數(shù)
- 對(duì)于任何異步函數(shù)例朱,你可以使用 Task 替代 void 作為返回類型,讓該方法成為更有效的異步(可以進(jìn)行 await)鱼蝉。
- 并不需要在方法體中顯式的返回 Task洒嗤。編譯器會(huì)生成一個(gè) Task(當(dāng)方法完成或發(fā)生異常時(shí)),這使得創(chuàng)建異步的調(diào)用鏈非常方便
- 編譯器會(huì)對(duì)返回 Task 的異步函數(shù)進(jìn)行擴(kuò)展蚀乔,使其成為當(dāng)發(fā)送信號(hào)或發(fā)送故障時(shí)使用 TaskCompletionSource 來(lái)創(chuàng)建 Task 的代碼烁竭。
- 因此,當(dāng)返回 Task 的異步方法結(jié)束的時(shí)候吉挣,執(zhí)行就會(huì)跳回到對(duì)它進(jìn)行 await 的地方派撕。
編寫異步函數(shù) 富客戶端場(chǎng)景下
- 富客戶端場(chǎng)景下,執(zhí)行在此刻會(huì)跳回到 UI 線程(如果目前不在 UI 線程的話)睬魂。
- 否則终吼,就在 continuation 返回的任意線程上繼續(xù)執(zhí)行。
- 這意味著氯哮,在異步調(diào)用圖中向上冒泡的時(shí)候际跪,不會(huì)發(fā)生延遲成本,除非是 UI 線程啟動(dòng)的第一次“反彈”喉钢。
返回 Task<TResult>
- 如果方法體返回 TResult姆打,那么異步方法就可以返回 Task<TResult>
- 其原理就是給 TaskCompletion 發(fā)送的信號(hào)帶有值,而不是 null肠虽。
- 與同步編程很相似幔戏,是故意這樣設(shè)計(jì)的。
C# 中如何設(shè)計(jì)異步函數(shù)
- 以同步的方式編寫方法
- 使用異步調(diào)用來(lái)代替同步調(diào)用税课,并且進(jìn)行 await
- 除了頂層方法外(UI 控件的 event handler)闲延,把你方法的返回類型升級(jí)為 Task 或者 Task<TResult>,這樣就可以進(jìn)行 await 了韩玩。
編譯器能對(duì)異步函數(shù)生成 Task 意味著什么垒玲?
- 大多數(shù)情況下,你只需要在初始化 IO-bound 并發(fā)的底層方法里顯式地初始化 TaskCompletionSource找颓,這種情況很少見(jiàn)合愈。
- 針對(duì)初始化 compute-bound 的并發(fā)方法,你可以使用 Task.Run 來(lái)創(chuàng)建 Task。
異步調(diào)用圖執(zhí)行
- 整個(gè)執(zhí)行與之前同步例子中調(diào)用圖順序一樣想暗,因?yàn)槲覀儗?duì)每個(gè)異步函數(shù)的調(diào)用都進(jìn)行了 await妇汗。
并行(Parallelism)
- 不使用 await 來(lái)調(diào)用異步函數(shù)會(huì)導(dǎo)致并行執(zhí)行的發(fā)生帘不。
- 例如:_button.Click += (sender, args) => Go();
- 確實(shí)也能滿足保持 UI 響應(yīng)的并發(fā)要求说莫。
- 同樣,可以并行跑兩個(gè)操作:
var task1 = PrintAnswerToLife();
var task2 = PrintAnswerToLife();
await task1; await task2;
異步 lambda 表達(dá)式
添加 async 關(guān)鍵字后寞焙,一樣的储狭。
調(diào)用方式也一樣。
附加 event handler 的時(shí)候也可以使用 Lambda 表達(dá)式捣郊,也可以返回 Task<TResult>辽狈。