c# 異步編程入門
一、什么算異步?
廣義來講,兩個工作流能同時進行就算異步,例如,CPU與外設(shè)之間的工作流就是異步的。在面向服務(wù)的系統(tǒng)中,各個子系統(tǒng)之間通信一般都是異步的,例如,訂單系統(tǒng)與支付系統(tǒng)之間的通信是異步的,又如,在現(xiàn)實生活中,你去館子吃飯,工作流是這樣的,點菜->下單->做你的事->上菜->吃飯,這個也是異步的,具體來講你和廚師之間是異步的,異步是如此重要,因外它代表者高效率(兩者或兩者以上的工作可以同時進行),但復(fù)雜,同步的世界簡單,但效率極極低。
二、在編程中的異步
在編程中,除了同步和異步這兩個名詞,還多了一個阻塞和非阻塞,其中,阻塞和非阻塞是針對線程的概念,那么同步和異步是針對誰呢?其實很多情況下同步和異步并沒有具體針對某一事物,所以導(dǎo)致了針對同步阻塞、同步非阻塞、異步阻塞、異步非阻塞這幾個概念的模糊不清。并且也確實沒有清晰的邊界,請看以下例子:
public static void DoWorkA() { Thread thread = new Thread(() => { Console.WriteLine("WorkA Done!"); }); thread.Start(); } public static void DoWordB() { Thread thread = new Thread(() => { Console.WriteLine("WorkB Done!"); }); thread.Start(); } static void Main(string[] args) { DoWorkA(); DoWordB(); }
假設(shè)運行該代碼的CPU是單核單線程,那么請問?DoWorkA()、DoWorkB()這兩個函數(shù)是異步的嗎?因為CPU是單核,所以根本不能同時運行兩個函數(shù),那么從這個層次來講,他們之間其實是同步的,但是,現(xiàn)實的情況是我們一般都認為他們之間是異步的,因為我們是從代碼的執(zhí)行順序角度考慮的,而不是從CPU本身的工作流程考慮的。所以要分上下文考慮。再請看下面這個例子:
static void Main(string[] args) { DoWorkA(); QueryDataBaseSync();//同步查詢數(shù)據(jù)庫 DoWorkB(); }
從代碼的執(zhí)行順序角度考慮,這三個函數(shù)執(zhí)行就是同步的,但是,從CPU的角度來講,數(shù)據(jù)庫查詢工作(另一臺機器)和CPU計算工作是異步的,在下文中,沒有做特別申明,則都是從代碼的執(zhí)行順序角度來討論同步和異步。
再解釋一下阻塞和非阻塞以及相關(guān)的知識:
阻塞特指線程由運行狀態(tài)轉(zhuǎn)換到掛起狀態(tài),但CPU并不會阻塞,操作系統(tǒng)會切換另一個處于就緒狀態(tài)的線程,并轉(zhuǎn)換成運行狀態(tài)。導(dǎo)致線程被阻塞的原因有很多,如:發(fā)生系統(tǒng)調(diào)用(應(yīng)用程序調(diào)用系統(tǒng)API,如果調(diào)用成功,會發(fā)生從應(yīng)用態(tài)->內(nèi)核態(tài)->應(yīng)用態(tài)的轉(zhuǎn)換開銷),但此時外部條件并沒有滿足,如從Socket內(nèi)核緩沖區(qū)讀數(shù)據(jù),此時緩沖區(qū)還沒有數(shù)據(jù),則會導(dǎo)致操作系統(tǒng)掛起該線程,切換到另一個處于就緒態(tài)的線程然后給CPU執(zhí)行,這是主動調(diào)用導(dǎo)致的,還有被動導(dǎo)致的,對于現(xiàn)在的分時操作系統(tǒng),在一個線程時間片到了之后,會發(fā)生時鐘中斷信號,然后由操作系統(tǒng)預(yù)先寫好的中斷函數(shù)處理,再按一定策略(如線程優(yōu)先級)切換至另一個線程執(zhí)行,導(dǎo)致線程被動地從運行態(tài)轉(zhuǎn)換成掛起狀態(tài)。
非阻塞一般指函數(shù)調(diào)用不會導(dǎo)致執(zhí)行該函數(shù)的線程從運行態(tài)轉(zhuǎn)換成掛起狀態(tài)。
三、原始的異步編程模式之回調(diào)函數(shù)#
在此之前,我們先稍微了解下圖形界面的工作原理,GUI程序大概可以用以下偽代碼表示:
While(GetMessage() != 'exit') //從線程消息隊列中獲取一個消息,線程消息隊列由系統(tǒng)維護,例如鼠標移動事件,這個事件由操作系統(tǒng)捕捉,并投遞到線程的消息隊列中。 { msg = TranslateMessage();//轉(zhuǎn)換消息格式 DispatherMessage(msg);//分發(fā)消息到相應(yīng)的處理函數(shù) }
其中DispatherMessage根據(jù)不同的消息類型,調(diào)用不同的消息處理函數(shù),例如鼠標移動消息(MouseMove),此時消息處理函數(shù)可以根據(jù)MouseMove消息中的值,做相應(yīng)的處理,例如調(diào)用繪圖相關(guān)函數(shù)畫出鼠標此刻的形狀。
一般來講,我們稱這個循環(huán)為消息循環(huán)(事件循環(huán)、EventLoop),編程模型稱為消息驅(qū)動模型(事件驅(qū)動),在UI程序中,執(zhí)行這部分代碼的線程一般只有一個線程,稱為UI線程,為什么是單線程,讀者可以去思考。
以上為背景知識?,F(xiàn)在,我們思考,假如在UI線程中執(zhí)行一個會導(dǎo)致UI線程被阻塞的操作,或者在UI線程執(zhí)行一個純CPU計算的工作,會發(fā)生什么樣的結(jié)果?如果執(zhí)行一個導(dǎo)致UI線程被阻塞的操作,那么這個消息循環(huán)就會被迫停止,導(dǎo)致相關(guān)的繪圖消息不能被相應(yīng)的消息處理函數(shù)處理,表現(xiàn)就是UI界面“假死”,直到UI線程被喚起。如果是純CPU計算的工作,那么也會導(dǎo)致其他消息不能被及時處理,也會導(dǎo)致界面“假死”現(xiàn)象。如何處理這種情況?寫異步代碼。
我們先用控制臺程序模擬這個UI程序,后面以此為基礎(chǔ)。
public static string GetMessage() { return Console.ReadLine(); } public static string TranslateMessage(string msg) { return msg; } public static void DispatherMessage(string msg) { switch (msg) { case "MOUSE_MOVE": { OnMOUSE_MOVE(msg); break; } default: break; } } public static void OnMOUSE_MOVE(string msg) { Console.WriteLine("開始繪制鼠標形狀"); } static void Main(string[] args) { while(true) { string msg = GetMessage(); if (msg == "quit") return; string m = TranslateMessage(msg); DispatherMessage(m); } }
1、回調(diào)函數(shù)
上面那個例子,一但外部有消息到來,根據(jù)不同的消息類型,調(diào)用不同的處理函數(shù),如鼠標移動時產(chǎn)生MOUSE_DOWN消息,相應(yīng)的消息處理函數(shù)就開始重新繪制鼠標的形狀,這樣一但你鼠標移動,就你會發(fā)現(xiàn)屏幕上的鼠標跟著移動了。
現(xiàn)在假設(shè)我們增加一個消息處理函數(shù),如OnMOUSE_DOWN,這個函數(shù)內(nèi)部進行了一個阻塞的操作,如發(fā)起一個HTTP請求,在HTTP請求回復(fù)到來前,該UI程序會“假死”,我們編寫異步代碼來解決這個問題。
public static int Http() { Thread.Sleep(1000);//模擬網(wǎng)絡(luò)IO延時 return 1; } public static void HttpAsync(Action<int> action,Action error) { //這里我們用另一個線程來實現(xiàn)異步IO,由于Http方法內(nèi)部是通過Sleep來模擬網(wǎng)絡(luò)IO延時的,這里也只能通過另一個線程來實現(xiàn)異步IO //但記住,多線程是實現(xiàn)異步IO的一個手段而已,它不是必須的,后面會講到如何通過一個線程來實現(xiàn)異步IO。 Thread thread = new Thread(() => { try { int res = Http(); action(res); } catch { error(); } }); thread.Start(); } public static void OnMouse_DOWN(string msg) { HttpAsync(res => { Console.WriteLine("請求成功!"); //使用該結(jié)果做一些工作 }, () => { Console.WriteLine("請求發(fā)生錯誤!"); }); }
此時界面不再“假死”了,我們看下代碼可讀性,感覺還行,但是,如果再在回調(diào)函數(shù)里面再發(fā)起類似的異步請求呢?(有人可能有疑問,為什么還需要發(fā)起異步請求,我發(fā)同步請求不行嗎?這都是在另一個線程里了。是的,在這個例子里是沒問題的,但真實情況是,執(zhí)行回調(diào)函數(shù)的代碼,一般都會在UI線程,因為取得結(jié)果后需要更新相關(guān)UI組件上的界面,例如文字,而更新界面的操作都是放在UI線程里的,如何把回調(diào)函數(shù)放到UI線程上執(zhí)行,這里不做討論,在.NET中,這跟同步上下文(Synchronization context)有關(guān),后面會講到),那么代碼會變成這樣
public static void OnMouse_DOWN(string msg) { HttpAsync(res => { Console.WriteLine("請求成功!"); //使用該結(jié)果做一些工作 HttpAsync(r1 => { //使用該結(jié)果做一些工作 HttpAsync(r2 => { //使用該結(jié)果做一些工作 }, () => { }); }, () => { }); }, () => { Console.WriteLine("請求發(fā)生錯誤!"); }); }
寫過JS的同學(xué)可能很清楚,這叫做“回調(diào)地獄”,如何解決這個問題?JS中有Promise,而C#中有Task,我們先用Task來寫這一段代碼,然后自己實現(xiàn)一個與Task功能差不多的簡單的類庫。
public static Task<int> HttpAsync() { return Task.Run(() => { return Http(); }); } public static void OnMouse_DOWN(string msg) { HttpAsync() .ContinueWith(t => { if(t.Status == TaskStatus.Faulted) { }else if(t.Status == TaskStatus.RanToCompletion) { //做一些工作 } }) .ContinueWith(t => { if (t.Status == TaskStatus.Faulted) { } else if (t.Status == TaskStatus.RanToCompletion) { //做一些工作 } }) .ContinueWith(t => { if (t.Status == TaskStatus.Faulted) { } else if (t.Status == TaskStatus.RanToCompletion) { //做一些工作 } }); }
是不是感覺清爽了許多?這是編寫異步代碼的第一個躍進。下篇將會介紹,如何自己實現(xiàn)一個簡單的Task。后面還會提到C#中async/await的本質(zhì)作用,async/await是怎么跟Task聯(lián)系起來的,怎么把自己寫的Task庫與async/await連結(jié)起來,以及一個線程如何實現(xiàn)異步IO。
覺得有收獲的不妨點個贊,有支持才有動力寫出更好的文章。
以上就是c# 異步編程入門的詳細內(nèi)容,更多關(guān)于c# 異步編程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#?中?List?與?List?多層嵌套不改變原值的實現(xiàn)方法(深度復(fù)制)
這篇文章主要介紹了C#?中?List?與?List?多層嵌套不改變原值的實現(xiàn)方法,使用?BinaryFormatter?將原始?List?序列化為字節(jié)流,然后再反序列化得到新的?List,實現(xiàn)了深度復(fù)制,需要的朋友可以參考下2024-03-03C#字符串?dāng)?shù)組轉(zhuǎn)換為整形數(shù)組的方法
這篇文章主要介紹了C#字符串?dāng)?shù)組轉(zhuǎn)換為整形數(shù)組的方法,涉及C#數(shù)組遍歷與轉(zhuǎn)換的相關(guān)技巧,需要的朋友可以參考下2015-06-06C#實現(xiàn)appSettings節(jié)點讀取與修改的方法
這篇文章主要介紹了C#實現(xiàn)appSettings節(jié)點讀取與修改的方法,是非常實用的技巧,需要的朋友可以參考下2014-10-10