C# 異步編程筆記

阻塞(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)前的線程)
  • 很難使用較小的并發(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ā)生故障
  • 它對(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è)線程
  • 富客戶端應(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 線程

經(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>辽狈。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市呛牲,隨后出現(xiàn)的幾起案子刮萌,更是在濱河造成了極大的恐慌,老刑警劉巖娘扩,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件着茸,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡琐旁,警方通過(guò)查閱死者的電腦和手機(jī)涮阔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)灰殴,“玉大人敬特,你說(shuō)我怎么就攤上這事∥眨” “怎么了伟阔?”我有些...
    開(kāi)封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)掰伸。 經(jīng)常有香客問(wèn)我皱炉,道長(zhǎng),這世上最難降的妖魔是什么碱工? 我笑而不...
    開(kāi)封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任娃承,我火速辦了婚禮,結(jié)果婚禮上怕篷,老公的妹妹穿的比我還像新娘历筝。我一直安慰自己,他們只是感情好廊谓,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布梳猪。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪春弥。 梳的紋絲不亂的頭發(fā)上呛哟,一...
    開(kāi)封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音匿沛,去河邊找鬼扫责。 笑死,一個(gè)胖子當(dāng)著我的面吹牛逃呼,可吹牛的內(nèi)容都是我干的鳖孤。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼抡笼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼苏揣!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起推姻,我...
    開(kāi)封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤平匈,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后藏古,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體增炭,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年校翔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了弟跑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡防症,死狀恐怖孟辑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蔫敲,我是刑警寧澤饲嗽,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站奈嘿,受9級(jí)特大地震影響貌虾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜裙犹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一尽狠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧叶圃,春花似錦袄膏、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春斥黑,著一層夾襖步出監(jiān)牢的瞬間揖盘,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工锌奴, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留兽狭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓缨叫,卻偏偏與公主長(zhǎng)得像椭符,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子耻姥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容