深入分析C#中的異步和多線程
許多開發(fā)人員對異步代碼和多線程以及它們的工作原理和使用方法都有錯誤的認識。在這里,你將了解這兩個概念之間的區(qū)別,并使用c#實現(xiàn)它們。
我:“服務(wù)員,這是我第一次來這家餐廳。通常需要4個小時才能拿到食物嗎?”
服務(wù)員:“哦,是的,先生。這家餐廳的廚房里只有一個廚師?!?/p>
我:“……只有一個廚師嗎?”
服務(wù)員:“是的,先生,我們有好幾個廚師,但每次只有一個在廚房工作?!?/p>
我:“所以其他10個穿著廚師服站在廚房里的人……什么都不做嗎?廚房太小了嗎?”
服務(wù)員:“哦,我們的廚房很大,先生?!?/p>
我:“那為什么他們不同時工作呢?”
服務(wù)員:“先生,這倒是個好主意,但我們還沒想好怎么做?!?/p>
我:“好了,奇怪。但是…嘿…現(xiàn)在的主廚在哪里?我現(xiàn)在沒看見有人在廚房里。”
服務(wù)員:“是的,先生。有一份訂單的廚房用品已經(jīng)用完了,所以廚師已經(jīng)停止烹飪,站在外面等著送貨了?!?/p>
我:“看起來他可以一邊等一邊做飯,也許送貨員可以直接告訴他們什么時候到了?”
服務(wù)員:“又是一個絕妙的主意,先生。我們在后面有送貨門鈴,但廚師喜歡等。我去給你再拿點水來?!?/p>
多糟糕的餐廳,對吧?不幸的是,很多程序都是這樣工作的。
有兩種不同的方法可以讓這家餐廳做得更好。
首先,很明顯,每個單獨的晚餐訂單可以由不同的廚師來處理。每一種都是一個必須按特定順序發(fā)生的事情列表(準備原料,然后混合它們,然后烹飪,等等)。因此,如果每個廚師都致力于處理這一清單上的東西,幾份晚餐訂單可以同時做出。
這是一個真實世界中的多線程示例。計算機有能力讓多個不同的線程同時運行,每個線程負責按特定順序執(zhí)行一系列活動。
然后還有異步行為。需要明確的是,異步不是多線程的。還記得那個一直在等外賣的廚師嗎?真是浪費時間!在等待的過程中,他沒有做任何有意義的事情,比如做飯。而且,等待也不會讓送貨更快。一旦他打電話訂購供應(yīng)品,發(fā)貨就會隨時發(fā)生,所以為什么要等呢?相反,送貨員只需按門鈴,說一句:“嘿,這是你的供應(yīng)品!”
有很多I/O活動是由代碼之外的東西處理的。例如,向遠程服務(wù)器發(fā)送一個網(wǎng)絡(luò)請求。這就像給餐廳點餐一樣。你的代碼所做的唯一事情就是進行調(diào)用并接收結(jié)果。如果選擇等待結(jié)果,在這兩者之間完全不做任何事情,那么這就是“同步”行為。
然而,如果你更喜歡在結(jié)果返回時被打斷/通知(就像送貨員到達時按門鈴),同時可以處理其他事情,那么這就是“異步”行為。
只要工作是由不受當前代碼直接控制的對象完成的,就可以使用異步代碼。例如,當你向硬盤驅(qū)動器寫入一堆數(shù)據(jù)時,你的代碼并沒有執(zhí)行實際的寫入操作。它只是請求硬件執(zhí)行該任務(wù)。因此,你可以使用異步編碼開始編寫,然后在編寫完成時得到通知,同時繼續(xù)處理其他事情。
異步的優(yōu)點在于不需要額外的線程,因此非常高效。
“等等!”你說?!叭绻麤]有額外的線程,那么誰或什么在等待結(jié)果?代碼如何知道返回的結(jié)果?”
還記得那個門鈴嗎?你的電腦里有一個系統(tǒng)叫做“中斷”系統(tǒng),它的工作原理有點像那個門鈴。當你的代碼開始一個異步活動時,它基本上會安裝一個虛擬的門鈴。當其他任務(wù)(寫入硬盤驅(qū)動器,等待網(wǎng)絡(luò)響應(yīng)等)完成時,中斷系統(tǒng)“中斷”當前運行的代碼并按下門鈴,讓你的應(yīng)用程序知道有一個任務(wù)在等待!不需要線程坐在那里等待!
讓我們快速回顧一下我們的兩種工具:
多線程:使用一個額外的線程來執(zhí)行一系列活動/任務(wù)。
異步:使用同一個線程和中斷系統(tǒng),讓線程外的其他組件完成一些活動,并在活動結(jié)束時得到通知。
UI線程
還有一件重要的事情需要知道的是為什么使用這些工具是好的。在.net中,有一個主線程叫做UI線程,它負責更新屏幕的所有可視部分。默認情況下,這是一切運行的地方。當你點擊一個按鈕,你想看到按鈕被短暫地按下,然后返回,這是UI線程的責任。你的應(yīng)用中只有一個UI線程,這意味著如果你的UI線程忙著做繁重的計算或等待網(wǎng)絡(luò)請求之類的事情,那么它不能更新你在屏幕上看到的東西,直到它完成。結(jié)果是,你的應(yīng)用程序看起來像“凍結(jié)”——你可以點擊一個按鈕,但似乎什么都不會發(fā)生,因為UI線程正在忙著做其他事情。
理想情況下,你希望UI線程盡可能地空閑,這樣你的應(yīng)用程序似乎總是在響應(yīng)用戶的操作。這就是異步和多線程的由來。通過使用這些工具,可以確保在其他地方完成繁重的工作,UI線程保持良好和響應(yīng)性。
現(xiàn)在讓我們看看如何在c#中使用這些工具。
C#的異步操作
執(zhí)行異步操作的代碼非常簡單。你應(yīng)該知道兩個主要的關(guān)鍵字:“async”和“await”,所以人們通常將其稱為async/await。假設(shè)你現(xiàn)在有這樣的代碼:
public void Loopy() { var hugeFiles = new string[] { "Gr8Gonzos_Home_Movie_In_8k_Res.mkv", // 1 GB "War_And_Peace_In_150_Languages.rtf", // 1.2 GB "Cats_On_Catnip.mpg" // 0.9 GB }; foreach (var hugeFile in hugeFiles) { ReadAHugeFile(hugeFile); } MessageBox.Show("All done!"); } public byte[] ReadAHugeFile(string bigFile) { var fileSize = new FileInfo(bigFile).Length; // Get the file size var allData = new byte[fileSize]; // Allocate a byte array as large as our file using (var fs = new System.IO.FileStream(bigFile, FileMode.Open)) { fs.Read(allData, 0, (int)fileSize); // Read the entire file... } return allData; // ...and return those bytes! }
在當前的形式中,這些都是同步運行的。如果你點擊一個按鈕從UI線程運行Loopy(),那么應(yīng)用程序?qū)⑺坪鮾鼋Y(jié),直到所有三大文件閱讀,因為每個“ReadAHugeFile”是要花很長時間在UI線程上運行,并將同步閱讀。這可不好!讓我們看看能否將ReadAHugeFile變?yōu)楫惒降倪@樣UI線程就能繼續(xù)處理其他東西。
無論何時,只要有支持異步的命令,微軟通常會給我們同步和異步版本的這些命令。在上面的代碼中,System.IO.FileStream對象同時具有"Read"和"ReadAsync"方法。所以第一步就是將“fs.Read”修改成“fs.ReadAsync”。
public byte[] ReadAHugeFile(string bigFile) { var fileSize = new FileInfo(bigFile).Length; // Get the file size var allData = new byte[fileSize]; // Allocate a byte array as large as our file using (var fs = new System.IO.FileStream(bigFile, FileMode.Open)) { fs.ReadAsync(allData, 0, (int)fileSize); // Read the entire file asynchronously... } return allData; // ...and return those bytes! }
如果現(xiàn)在運行它,它會立即返回,并且“allData”字節(jié)數(shù)組中不會有任何數(shù)據(jù)。為什么?
這是因為ReadAsync是開始讀取并返回一個任務(wù)對象,這有點像一個書簽。這是.net的一個“Promise”,一旦異步活動完成(例如從硬盤讀取數(shù)據(jù)),它將返回結(jié)果,任務(wù)對象可以用來訪問結(jié)果。但如果我們對這個任務(wù)不做任何事情,那么系統(tǒng)就會立即繼續(xù)到下一行代碼,也就是我們的"return allData"行,它會返回一個尚未填滿數(shù)據(jù)的數(shù)組。
因此,告訴代碼等待結(jié)果是很有用的(但這樣一來,原始線程可以在此期間繼續(xù)做其他事情)。為了做到這一點,我們使用了一個"awaiter",它就像在async調(diào)用之前添加單詞"await"一樣簡單:
public byte[] ReadAHugeFile(string bigFile) { var fileSize = new FileInfo(bigFile).Length; // Get the file size var allData = new byte[fileSize]; // Allocate a byte array as large as our file using (var fs = new System.IO.FileStream(bigFile, FileMode.Open)) { await fs.ReadAsync(allData, 0, (int)fileSize); // Read the entire file asynchronously... } return allData; // ...and return those bytes! }
如果現(xiàn)在運行它,它會立即返回,并且“allData”字節(jié)數(shù)組中不會有任何數(shù)據(jù)。為什么?
這是因為ReadAsync是開始讀取并返回一個任務(wù)對象,這有點像一個書簽。這是.net的一個“Promise”,一旦異步活動完成(例如從硬盤讀取數(shù)據(jù)),它將返回結(jié)果,任務(wù)對象可以用來訪問結(jié)果。但如果我們對這個任務(wù)不做任何事情,那么系統(tǒng)就會立即繼續(xù)到下一行代碼,也就是我們的"return allData"行,它會返回一個尚未填滿數(shù)據(jù)的數(shù)組。
因此,告訴代碼等待結(jié)果是很有用的(但這樣一來,原始線程可以在此期間繼續(xù)做其他事情)。為了做到這一點,我們使用了一個"awaiter",它就像在async調(diào)用之前添加單詞"await"一樣簡單:
public byte[] ReadAHugeFile(string bigFile) { var fileSize = new FileInfo(bigFile).Length; // Get the file size var allData = new byte[fileSize]; // Allocate a byte array as large as our file using (var fs = new System.IO.FileStream(bigFile, FileMode.Open)) { await fs.ReadAsync(allData, 0, (int)fileSize); // Read the entire file asynchronously... } return allData; // ...and return those bytes! }
哦。如果你試過,你會發(fā)現(xiàn)有一個錯誤。這是因為.net需要知道這個方法是異步的,它最終會返回一個字節(jié)數(shù)組。因此,我們做的第一件事是在返回類型之前添加單詞“async”,然后用Task<…>,是這樣的:
public async Task<byte[]> ReadAHugeFile(string bigFile) { var fileSize = new FileInfo(bigFile).Length; // Get the file size var allData = new byte[fileSize]; // Allocate a byte array as large as our file using (var fs = new System.IO.FileStream(bigFile, FileMode.Open)) { await fs.ReadAsync(allData, 0, (int)fileSize); // Read the entire file asynchronously... } return allData; // ...and return those bytes! }
好吧!現(xiàn)在我們烹飪!如果我們現(xiàn)在運行我們的代碼,它將繼續(xù)在UI線程上運行,直到我們到達ReadAsync方法的await。此時,. net知道這是一個將由硬盤執(zhí)行的活動,因此“await”將一個小書簽放在當前位置,然后UI線程返回到它的正常處理(所有的視覺更新等)。
隨后,一旦硬盤驅(qū)動器讀取了所有數(shù)據(jù),ReadAsync方法將其全部復(fù)制到allData字節(jié)數(shù)組中,任務(wù)現(xiàn)在就完成了,因此系統(tǒng)按門鈴,讓原始線程知道結(jié)果已經(jīng)準備好了。原始線程說:“太棒了!讓我回到離開的地方!”一有機會,它就會回到“await fs.ReadSync”,然后繼續(xù)下一步,返回allData數(shù)組,這個數(shù)組現(xiàn)在已經(jīng)填充了我們的數(shù)據(jù)。
如果你在一個接一個地看一個例子,并且使用的是最近的Visual Studio版本,你會注意到這一行:
ReadAHugeFile(hugeFile);
…現(xiàn)在,它用綠色下劃線表示,如果將鼠標懸停在它上面,它會說,“因為這個調(diào)用沒有被等待,所以在調(diào)用完成之前,當前方法的執(zhí)行將繼續(xù)?!笨紤]對調(diào)用的結(jié)果應(yīng)用'await'操作符。"
這是Visual Studio讓你知道它承認ReadAHugeFile()是一個異步的方法,而不是返回一個結(jié)果,這也是返回任務(wù),所以如果你想等待結(jié)果,然后你就可以添加一個“await”:
await ReadAHugeFile(hugeFile);
…但如果我們這樣做了,那么你還必須更新方法簽名:
public async void Loopy()
注意,如果我們在一個不返回任何東西的方法上(void返回類型),那么我們不需要將返回類型包裝在Task<…>中。
但是,我們不要這樣做。相反,讓我們來了解一下我們可以用異步做些什么。
如果你不想等待ReadAHugeFile(hugeFile)的結(jié)果,因為你可能不關(guān)心最終的結(jié)果,但你不喜歡綠色下劃線/警告,你可以使用一個特殊的技巧來告訴.net。只需將結(jié)果賦給_字符,就像這樣:
_ = ReadAHugeFile(hugeFile);
這就是.net的語法,表示“我不在乎結(jié)果,但我不希望用它的警告來打擾我?!?/p>
好吧,我們試試別的。如果我們在這一行上使用了await,那么它將等待第一個文件被異步讀取,然后等待第二個文件被異步讀取,最后等待第三個文件被異步讀取。但是…如果我們想要同時異步地讀取所有3個文件,然后在所有3個文件都完成之后,我們允許代碼繼續(xù)到下一行,該怎么辦?
有一個叫做Task.WhenAll()的方法,它本身是一個你可以await的異步方法。傳入其他任務(wù)對象的列表,然后等待它,一旦所有任務(wù)都完成,它就會完成。所以最簡單的方法就是創(chuàng)建一個List<Task>對象:
List<Task> readingTasks = new List<Task>();
…然后,當我們將每個ReadAHugeFile()調(diào)用中的Task添加到列表中時:
foreach (var hugeFile in hugeFiles) { readingTasks.Add(ReadAHugeFile(hugeFile)); }
…最后我們 await Task.WhenAll():
await Task.WhenAll(readingTasks);
最終的方法是這樣的:
public async void Loopy() { var hugeFiles = new string[] { "Gr8Gonzos_Home_Movie_In_8k_Res.mkv", // 1 GB "War_And_Peace_In_150_Languages.rtf", // 1.2 GB "Cats_On_Catnip.mpg" // 0.9 GB }; List<Task> readingTasks = new List<Task>(); foreach (var hugeFile in hugeFiles) { readingTasks.Add(ReadAHugeFile(hugeFile)); } await Task.WhenAll(readingTasks); MessageBox.Show(sb.ToString()); }
當涉及到并行活動時,一些I/O機制比其他機制工作得更好(例如,網(wǎng)絡(luò)請求通常比硬盤讀取工作得更好,但這取決于硬件),但原理是相同的。
現(xiàn)在,“await”操作符還要做的最后一件事是提取最終結(jié)果。所以在上面的例子中,ReadAHugeFile返回一個任務(wù)<byte[]>。await的神奇功能會在完成后自動拋出Task<>包裝器,并返回byte[]數(shù)組,所以如果你想訪問Loopy()中的字節(jié),你可以這樣做:
byte[] data = await ReadAHugeFile(hugeFile);
再次強調(diào),await是一個神奇的小命令,它使異步編程變得非常簡單,并為你處理各種各樣的小事情。
現(xiàn)在讓我們轉(zhuǎn)向多線程。
C#中的多線程
微軟有時會給你10種不同的方法來做同樣的事情,這就是它如何使用多線程。你有BackgroundWorker類、Thread和Task(它們有幾個變體)。最終,它們都做著相同的事情,只是有不同的功能?,F(xiàn)在,大多數(shù)人都使用Task,因為它們的設(shè)置和使用都很簡單,而且如果你想這樣做的話(我們稍后會講到),它們也可以很好地與異步代碼交互。如果你好奇的話,關(guān)于這些具體區(qū)別有很多文章,但是我們在這里使用任務(wù)。
要讓任何方法在單獨的線程中運行,只需使用Task.Run()方法來執(zhí)行它。例如,假設(shè)你有這樣一個方法:
public void DoRandomCalculations(int howMany) { var rng = new Random(); for (int i = 0; i < howMany; i++) { int a = rng.Next(1, 1000); int b = rng.Next(1, 1000); int sum = 0; sum = a + b; } }
我們可以像這樣在當前線程中調(diào)用它:
DoRandomCalculations(1000000);
或者我們可以讓另一個線程來做這個工作:
Task.Run(() => DoRandomCalculations(1000000));
當然,有一些不同的版本,但這是總體思路。
Task. run()的一個優(yōu)點是它返回一個我們可以等待的任務(wù)對象。因此,如果想在一個單獨的線程中運行一堆代碼,然后在進入下一步之前等待它完成,你可以使用await,就像你在前面一節(jié)看到的那樣:
var finalData = await Task.Run(() => {});
請記住,本文討論的是如何開始,以及這些概念是如何工作的,但它并不是全面的。但是也許有了這些知識,你將能夠理解其他人關(guān)于多線程和異步編碼更高級種類的更復(fù)雜的文章。
以上就是深入分析C#中的異步和多線程的詳細內(nèi)容,更多關(guān)于C#中的異步和多線程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Unity3D游戲開發(fā)數(shù)據(jù)持久化PlayerPrefs的用法詳解
在本篇文章里小編給大家整理了關(guān)于Unity3D游戲開發(fā)之數(shù)據(jù)持久化PlayerPrefs的使用的相關(guān)知識點內(nèi)容,需要的朋友們參考下。2019-08-08Unity 使用TexturePacker打包圖集的操作方法
這篇文章主要介紹了Unity 使用TexturePacker打包圖集的操作方法,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-08-08C#生成指定范圍內(nèi)的不重復(fù)隨機數(shù)
對于隨機數(shù),大家都知道,計算機不 可能產(chǎn)生完全隨機的數(shù)字,所謂的隨機數(shù)發(fā)生器都是通過一定的算法對事先選定的隨機種子做復(fù)雜的運算,用產(chǎn)生的結(jié)果來近似的模擬完全隨機數(shù),這種隨機數(shù)被稱 作偽隨機數(shù)。偽隨機數(shù)是以相同的概率從一組有限的數(shù)字中選取的。2015-05-05