許多開發人員對異步代碼和多線程以及它們的工做原理和使用方法都有錯誤的認識。在這裏,你將瞭解這兩個概念之間的區別,並使用c#實現它們。html
我:「服務員,這是我第一次來這家餐廳。一般須要4個小時才能拿到食物嗎?」編程
服務員:「哦,是的,先生。這家餐廳的廚房裏只有一個廚師。」c#
我:「……只有一個廚師嗎?」數組
服務員:「是的,先生,咱們有好幾個廚師,但每次只有一個在廚房工做。」服務器
我:「因此其餘10個穿着廚師服站在廚房裏的人……什麼都不作嗎?廚房過小了嗎?」網絡
服務員:「哦,咱們的廚房很大,先生。」多線程
我:「那爲何他們不一樣時工做呢?」app
服務員:「先生,這卻是個好主意,但咱們還沒想好怎麼作。」dom
我:「好了,奇怪。可是…嘿…如今的主廚在哪裏?我如今沒看見有人在廚房裏。」異步
服務員:「是的,先生。有一份訂單的廚房用品已經用完了,因此廚師已經中止烹飪,站在外面等着送貨了。」
我:「看起來他能夠一邊等一邊作飯,也許送貨員能夠直接告訴他們何時到了?」
服務員:「又是一個絕妙的主意,先生。咱們在後面有送貨門鈴,但廚師喜歡等。我去給你再拿點水來。」
多糟糕的餐廳,對吧?不幸的是,不少程序都是這樣工做的。
有兩種不一樣的方法可讓這家餐廳作得更好。
首先,很明顯,每一個單獨的晚餐訂單能夠由不一樣的廚師來處理。每一種都是一個必須按特定順序發生的事情列表(準備原料,而後混合它們,而後烹飪,等等)。所以,若是每一個廚師都致力於處理這一清單上的東西,幾份晚餐訂單能夠同時作出。
這是一個真實世界中的多線程示例。計算機有能力讓多個不一樣的線程同時運行,每一個線程負責按特定順序執行一系列活動。
而後還有異步行爲。須要明確的是,異步不是多線程的。還記得那個一直在等外賣的廚師嗎?真是浪費時間!在等待的過程當中,他沒有作任何有意義的事情,好比作飯。並且,等待也不會讓送貨更快。一旦他打電話訂購供應品,發貨就會隨時發生,因此爲何要等呢?相反,送貨員只需按門鈴,說一句:「嘿,這是你的供應品!」
有不少I/O活動是由代碼以外的東西處理的。例如,向遠程服務器發送一個網絡請求。這就像給餐廳點餐同樣。你的代碼所作的惟一事情就是進行調用並接收結果。若是選擇等待結果,在這二者之間徹底不作任何事情,那麼這就是「同步」行爲。
然而,若是你更喜歡在結果返回時被打斷/通知(就像送貨員到達時按門鈴),同時能夠處理其餘事情,那麼這就是「異步」行爲。
只要工做是由不受當前代碼直接控制的對象完成的,就可使用異步代碼。例如,當你向硬盤驅動器寫入一堆數據時,你的代碼並無執行實際的寫入操做。它只是請求硬件執行該任務。所以,你可使用異步編碼開始編寫,而後在編寫完成時獲得通知,同時繼續處理其餘事情。
異步的優勢在於不須要額外的線程,所以很是高效。
「等等!」你說。「若是沒有額外的線程,那麼誰或什麼在等待結果?代碼如何知道返回的結果?」
還記得那個門鈴嗎?你的電腦裏有一個系統叫作「中斷」系統,它的工做原理有點像那個門鈴。當你的代碼開始一個異步活動時,它基本上會安裝一個虛擬的門鈴。當其餘任務(寫入硬盤驅動器,等待網絡響應等)完成時,中斷系統「中斷」當前運行的代碼並按下門鈴,讓你的應用程序知道有一個任務在等待!不須要線程坐在那裏等待!
讓咱們快速回顧一下咱們的兩種工具:
多線程:使用一個額外的線程來執行一系列活動/任務。
異步:使用同一個線程和中斷系統,讓線程外的其餘組件完成一些活動,並在活動結束時獲得通知。
UI線程
還有一件重要的事情須要知道的是爲何使用這些工具是好的。在.net中,有一個主線程叫作UI線程,它負責更新屏幕的全部可視部分。默認狀況下,這是一切運行的地方。當你點擊一個按鈕,你想看到按鈕被短暫地按下,而後返回,這是UI線程的責任。你的應用中只有一個UI線程,這意味着若是你的UI線程忙着作繁重的計算或等待網絡請求之類的事情,那麼它不能更新你在屏幕上看到的東西,直到它完成。結果是,你的應用程序看起來像「凍結」——你能夠點擊一個按鈕,但彷佛什麼都不會發生,由於UI線程正在忙着作其餘事情。
理想狀況下,你但願UI線程儘量地空閒,這樣你的應用程序彷佛老是在響應用戶的操做。這就是異步和多線程的由來。經過使用這些工具,能夠確保在其餘地方完成繁重的工做,UI線程保持良好和響應性。
如今讓咱們看看如何在c#中使用這些工具。
C#的異步操做
執行異步操做的代碼很是簡單。你應該知道兩個主要的關鍵字:「async」和「await」,因此人們一般將其稱爲async/await。假設你如今有這樣的代碼:
在當前的形式中,這些都是同步運行的。若是你點擊一個按鈕從UI線程運行Loopy(),那麼應用程序將彷佛凍結,直到全部三大文件閱讀,由於每一個「ReadAHugeFile」是要花很長時間在UI線程上運行,並將同步閱讀。這可很差!讓咱們看看可否將ReadAHugeFile變爲異步的這樣UI線程就能繼續處理其餘東西。
不管什麼時候,只要有支持異步的命令,微軟一般會給咱們同步和異步版本的這些命令。在上面的代碼中,System.IO.FileStream對象同時具備"Read"和"ReadAsync"方法。因此第一步就是將「fs.Read」修改爲「fs.ReadAsync」。
若是如今運行它,它會當即返回,而且「allData」字節數組中不會有任何數據。爲何?
這是由於ReadAsync是開始讀取並返回一個任務對象,這有點像一個書籤。這是.net的一個「Promise」,一旦異步活動完成(例如從硬盤讀取數據),它將返回結果,任務對象能夠用來訪問結果。但若是咱們對這個任務不作任何事情,那麼系統就會當即繼續到下一行代碼,也就是咱們的"return allData"行,它會返回一個還沒有填滿數據的數組。
所以,告訴代碼等待結果是頗有用的(但這樣一來,原始線程能夠在此期間繼續作其餘事情)。爲了作到這一點,咱們使用了一個"awaiter",它就像在async調用以前添加單詞"await"同樣簡單:
哦。若是你試過,你會發現有一個錯誤。這是由於.net須要知道這個方法是異步的,它最終會返回一個字節數組。所以,咱們作的第一件事是在返回類型以前添加單詞「async」,而後用Task<…>,是這樣的:
好吧!如今咱們烹飪!若是咱們如今運行咱們的代碼,它將繼續在UI線程上運行,直到咱們到達ReadAsync方法的await。此時,. net知道這是一個將由硬盤執行的活動,所以「await」將一個小書籤放在當前位置,而後UI線程返回到它的正常處理(全部的視覺更新等)。
隨後,一旦硬盤驅動器讀取了全部數據,ReadAsync方法將其所有複製到allData字節數組中,任務如今就完成了,所以系統按門鈴,讓原始線程知道結果已經準備好了。原始線程說:「太棒了!讓我回到離開的地方!」一有機會,它就會回到「await fs.ReadSync」,而後繼續下一步,返回allData數組,這個數組如今已經填充了咱們的數據。
若是你在一個接一個地看一個例子,而且使用的是最近的Visual Studio版本,你會注意到這一行:
…如今,它用綠色下劃線表示,若是將鼠標懸停在它上面,它會說,「由於這個調用沒有被等待,因此在調用完成以前,當前方法的執行將繼續。」考慮對調用的結果應用'await'操做符。"
這是Visual Studio讓你知道它認可ReadAHugeFile()是一個異步的方法,而不是返回一個結果,這也是返回任務,因此若是你想等待結果,而後你就能夠添加一個「await」:
…但若是咱們這樣作了,那麼你還必須更新方法簽名:
注意,若是咱們在一個不返回任何東西的方法上(void返回類型),那麼咱們不須要將返回類型包裝在Task<…>中。
可是,咱們不要這樣作。相反,讓咱們來了解一下咱們能夠用異步作些什麼。
若是你不想等待ReadAHugeFile(hugeFile)的結果,由於你可能不關心最終的結果,但你不喜歡綠色下劃線/警告,你可使用一個特殊的技巧來告訴.net。只需將結果賦給_字符,就像這樣:
這就是.net的語法,表示「我不在意結果,但我不但願用它的警告來打擾我。」
好吧,咱們試試別的。若是咱們在這一行上使用了await,那麼它將等待第一個文件被異步讀取,而後等待第二個文件被異步讀取,最後等待第三個文件被異步讀取。可是…若是咱們想要同時異步地讀取全部3個文件,而後在全部3個文件都完成以後,咱們容許代碼繼續到下一行,該怎麼辦?
有一個叫作Task.WhenAll()的方法,它自己是一個你能夠await的異步方法。傳入其餘任務對象的列表,而後等待它,一旦全部任務都完成,它就會完成。因此最簡單的方法就是建立一個List<Task>對象:
…而後,當咱們將每一個ReadAHugeFile()調用中的Task添加到列表中時:
…最後咱們 await Task.WhenAll():
最終的方法是這樣的:
當涉及到並行活動時,一些I/O機制比其餘機制工做得更好(例如,網絡請求一般比硬盤讀取工做得更好,但這取決於硬件),但原理是相同的。
如今,「await」操做符還要作的最後一件事是提取最終結果。因此在上面的例子中,ReadAHugeFile返回一個任務<byte[]>。await的神奇功能會在完成後自動拋出Task<>包裝器,並返回byte[]數組,因此若是你想訪問Loopy()中的字節,你能夠這樣作:
再次強調,await是一個神奇的小命令,它使異步編程變得很是簡單,併爲你處理各類各樣的小事情。
如今讓咱們轉向多線程。
C#中的多線程
微軟有時會給你10種不一樣的方法來作一樣的事情,這就是它如何使用多線程。你有BackgroundWorker類、Thread和Task(它們有幾個變體)。最終,它們都作着相同的事情,只是有不一樣的功能。如今,大多數人都使用Task,由於它們的設置和使用都很簡單,並且若是你想這樣作的話(咱們稍後會講到),它們也能夠很好地與異步代碼交互。若是你好奇的話,關於這些具體區別有不少文章,可是咱們在這裏使用任務。
要讓任何方法在單獨的線程中運行,只需使用Task.Run()方法來執行它。例如,假設你有這樣一個方法:
咱們能夠像這樣在當前線程中調用它:
或者咱們可讓另外一個線程來作這個工做:
固然,有一些不一樣的版本,但這是整體思路。
Task. run()的一個優勢是它返回一個咱們能夠等待的任務對象。所以,若是想在一個單獨的線程中運行一堆代碼,而後在進入下一步以前等待它完成,你可使用await,就像你在前面一節看到的那樣:
請記住,本文討論的是如何開始,以及這些概念是如何工做的,但它並非全面的。可是也許有了這些知識,你將可以理解其餘人關於多線程和異步編碼更高級種類的更復雜的文章。
歡迎關注個人公衆號,若是你有喜歡的外文技術文章,能夠經過公衆號留言推薦給我。
原文連接:https://www.experts-exchange.com/articles/35473/Async-and-Multi-Threading-in-C-in-Plain-English.html