前言 對于服務端,達到高性能、高擴展離不開異步,對于客戶端,函式執行時間是1毫秒還是100毫秒差別不大,沒必要為這一點點時間煞費苦心,對于異步,好多人還有誤解,如: 異步就是多執行緒;異步就是如何利用好執行緒池,異步不是這么簡單,否則微軟沒必要在異步上花費這么多心思,本文就介紹異步最新的實作方式:Task,并自己動手寫一個異步IO函式,只有了解了異步函式內部實作方式,才能更好的利用它,
對于c#,異步處理經過了多個階段,但是對于現階段異步就是Task,微軟用Task來抽象異步操作,以后的異步函式,處理的都是Task,你會看到處處都是task的身影,為了處理Task,c#引入了兩個關鍵詞async,await,這兩個關鍵詞也可以說是一個關鍵詞,因為async的存在是為了表明await是關鍵詞,總而言之:兩個關鍵詞干了一件事,async關鍵詞并不改變函式的宣告,
有人說await就是語法糖,不值得大書特書,我只能說你錯了,軟體開發堅持的原則為:代碼要省,代碼要清晰易懂!如果沒有語法糖,代碼的維護性大大降低,await這個語法糖做的事很多;如果不用await,處理同樣的邏輯,需要多寫很多代碼,并導致邏輯不清晰,
Task的分類
異步分為兩類 compute-base 和 IO-base,compute-base就是計算密集型,函式所有的操作都是在記憶體中,不涉及IO;如果運行這個函式,則單個執行緒利用率達100%;IO-base就是涉及到IO,IO包括檔案讀寫,socket讀寫;這類異步操作底層涉及到IOCP(完成埠),相應的,Task也分為兩類,
對于這兩個區別可以舉個例子來區分:一臺電腦為4個執行緒,如果同時有4個compute-base執行緒運行,cpu的利用率為100%,如果同時有4個 IO-base的異步操作,cpu利用率可能遠遠低于100%,
對于.net 庫,有些函式會有兩個版本:一個是同步操作,一個是異步操作(函式名以Async結尾,回傳值為Task),舉個例子:

這是WebClient類獲取網址內容函式,你會問DownloadStringTaskAsync是compute-base Task,還是 IO-base Task?我可以肯定的告訴你:只要是.net基本類別庫提供的異步函式基本都是IO-base Task(微軟官方檔案是這樣要求),其實這樣要求是有道理的:對于compute-base異步,比較容易封裝;再者,這樣的異步是不能大規模的并發的,如果16個執行緒cpu,同時并發16個這樣的異步操作就是上限了;如果再多,反而會有害!
有人說,如果基本類別庫不提供 IO-base Task函式,我也可以封裝一下,這個也不難啊!代碼如下:
//把一個同步操作,改造成異步
public static async Task<byte[]> DownloadDataAsync(string url)
{
WebRequest request = WebRequest.Create(url);
return await Task.Run(() =>
{
using (var response = request.GetResponse())
using (var responseStream = response.GetResponseStream())
using (var result = new MemoryStream())
{
responseStream.CopyTo(result);
return result.ToArray();
}
});
}
上面函式如果說是異步操作,也不錯,但是,這不是“好”的異步操作!這是異步操作中夾雜著同步IO,會導致執行緒等待,如果有100個這樣的異步操作,就需要100個執行緒,這些執行緒大部分并沒在干活,而是在等待! 對于“好”的異步IO,如果同時有100個操作,甚至幾萬個操作,使用的執行緒都是有限的,一般不超過cpu執行緒數,這是怎么實作的?這涉及到IOCP,說起來有些復雜,可以參考IOCP相關資料,類別庫提供異步IO操作,都是涉及到IOCP的,所以得到如下結論: 如果類別庫不提供IO異步函式,無論怎么改造,不可能改造成“好”的異步函式!
Task實作的基本原理
Task變數狀態如下

狀態簡要分為生成、執行、執行完畢這三個階段,如果執行完畢前獲取執行后的值Task.Result,函式就會阻塞,那我怎么知道什么時候完成,而又不阻塞?有兩種辦法,輪詢和回呼通知,Task.IsCompleted屬性會指示函式是否執行完畢,輪詢不是一個好的辦法,采用回呼通知是上策!
回呼通知有個缺點:處理邏輯不直觀,回呼函式與異步呼叫函式不在一塊,還有可能隔著很多行代碼或不在同一個檔案,如果這樣的回呼函式太多,對理解代碼邏輯造成困難,代碼不易維護,微軟也考慮到了這個問題,那就用await關鍵詞來解決,await幫你處理了回呼函式的弊端,其實await后面的代碼與await前面的代碼不屬于同一個函式!await后面的代碼就是回呼函式!微軟確實給我們解決了這個問題,但是又帶來另一個問題,好多人不明白,明明是同一個函式,怎么實作了等待而又不阻塞當前執行緒!歸根到底,還是要理解await背后幫你干了啥,否則就會一直困惑,
要生成Task變數,只要理解幾個關鍵的處理步驟就行了,TaskCompletionSource類會幫助我們生成Task,如果IO完成,設定Task的狀態為完成就行了,后面,就會執行回呼函式(await關鍵詞幫我干了,你看不到回呼)!
如何寫一個IO-base Task函式?
大部分情況下不需要自己寫這樣的函式,但是,人是有好奇心的,如果不明白函式實作的原理,總是感覺不能釋懷!再者,明白函式實作原理,就能更好的利用這類函式,下面講解一下如何利用IOCP來實作異步函式,我沒有參考.net的原始碼,只是根據邏輯推理應該這實作,肯定和.net原始碼實作有出入,我寫這些代碼主要為了闡明Task實作原理,
IOCP處理邏輯

對于IOCP,這里不展開來講了,否則就跑題了,以socket讀取為例子,簡單總結一下:如果你要接收100個位元組的資料,你告訴IOCP你要接收100個位元組資料,并提供100個位元組的buffer,函式立即回傳;資料到達后,IOCP通知你,資料到了,資料就存在你提供的buffer里,
實作異步IO偽代碼如下:
class AyncInside { //完成埠句柄 IntPtr iocpHandle = IntPtr.Zero; Task<byte[]> ReadFromSocket(int count) { //生成此次操作需要相關資料 TaskCompletionSourceRead readInfo = new TaskCompletionSourceRead(); readInfo.Buffer = new byte[count]; //如果沒生成iocp則生成, if (iocpHandle == IntPtr.Zero) { iocpHandle = CreateIocp(); } // 告訴iocp,要讀取count位元組資料,函式不會阻塞,會立即回傳 //從完成埠收到資料后,會呼叫ReadScoketCallback //我們把readInfo也傳給函式,當回呼時,該變數會傳給回呼函式, ReadFromIocp(iocpHandle, readInfo.Buffer, readInfo, ReadScoketCallback); return readInfo.Tcs.Task; } void ReadScoketCallback(byte[] buffer, int readCount,object tag) { //tag就是呼叫ReadFromIocp時,傳的readInfo //便于我們知道異步呼叫時的背景關系資料, TaskCompletionSourceRead readInfo = tag as TaskCompletionSourceRead; if(buffer.Length == readCount ) { //呼叫完SetResult后,await后面的代碼就會被執行! readInfo.Tcs.SetResult(buffer); } else if (buffer.Length > 0) { Array.Resize(ref buffer, readCount); readInfo.Tcs.SetResult(buffer); } else { readInfo.Tcs.TrySetException(new Exception("讀取資料例外!socket可能已斷開!")); } } private void ReadFromIocp(IntPtr iocpHandle, byte[] buffer, object tag, Action<byte[] , int,object> readScoketCallback) { throw new NotImplementedException(); } private IntPtr CreateIocp() { throw new NotImplementedException(); } } //封裝異步讀取需要的資料 class TaskCompletionSourceRead { public TaskCompletionSource<byte[]> Tcs { get; set; } public byte[] Buffer { get; set; } }
上述代碼與實際可使用代碼差距還很大,我在這里主要為了闡明原理,通過上面的代碼,我們可以看到,這個異步函式并沒生成新的執行緒;網卡驅動和IOCP配合,幫我們接收了資料,所以這種方式才是真正可擴展的異步IO,
后記 異步IO和可擴展服務緊密關聯,對于.net core平臺,你會看到很多函式都是異步的,理解和用好異步IO函式非常重要,本文通過自己對異步IO的理解,試圖通過代碼闡明異步IO實作原理,希望你看過此文后,能對此有更深的理解!如果此文對你有所裨益,希望您給點個贊!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/62029.html
標籤:其他
