基于c# Task自己動(dòng)手寫個(gè)異步IO函數(shù)
前言
對(duì)于服務(wù)端,達(dá)到高性能、高擴(kuò)展離不開異步。對(duì)于客戶端,函數(shù)執(zhí)行時(shí)間是1毫秒還是100毫秒差別不大,沒(méi)必要為這一點(diǎn)點(diǎn)時(shí)間煞費(fèi)苦心。對(duì)于異步,好多人還有誤解,如: 異步就是多線程;異步就是如何利用好線程池。異步不是這么簡(jiǎn)單,否則微軟沒(méi)必要在異步上花費(fèi)這么多心思。本文就介紹異步最新的實(shí)現(xiàn)方式:Task,并自己動(dòng)手寫一個(gè)異步IO函數(shù)。只有了解了異步函數(shù)內(nèi)部實(shí)現(xiàn)方式,才能更好的利用它。
對(duì)于c#,異步處理經(jīng)過(guò)了多個(gè)階段,但是對(duì)于現(xiàn)階段異步就是Task,微軟用Task來(lái)抽象異步操作。以后的異步函數(shù),處理的都是Task。你會(huì)看到處處都是task的身影。為了處理Task,c#引入了兩個(gè)關(guān)鍵詞async,await。這兩個(gè)關(guān)鍵詞也可以說(shuō)是一個(gè)關(guān)鍵詞,因?yàn)閍sync的存在是為了表明await是關(guān)鍵詞??偠灾簝蓚€(gè)關(guān)鍵詞干了一件事,async關(guān)鍵詞并不改變函數(shù)的聲明。
有人說(shuō)await就是語(yǔ)法糖,不值得大書特書,我只能說(shuō)你錯(cuò)了。軟件開發(fā)堅(jiān)持的原則為:代碼要省,代碼要清晰易懂!如果沒(méi)有語(yǔ)法糖,代碼的維護(hù)性大大降低。await這個(gè)語(yǔ)法糖做的事很多;如果不用await,處理同樣的邏輯,需要多寫很多代碼,并導(dǎo)致邏輯不清晰。
Task的分類
異步分為兩類 compute-base 和 IO-base。compute-base就是計(jì)算密集型,函數(shù)所有的操作都是在內(nèi)存中,不涉及IO;如果運(yùn)行這個(gè)函數(shù),則單個(gè)線程利用率達(dá)100%;IO-base就是涉及到IO,IO包括文件讀寫,socket讀寫;這類異步操作底層涉及到IOCP(完成端口)。相應(yīng)的,Task也分為兩類。
對(duì)于這兩個(gè)區(qū)別可以舉個(gè)例子來(lái)區(qū)分:一臺(tái)電腦為4個(gè)線程。如果同時(shí)有4個(gè)compute-base線程運(yùn)行,cpu的利用率為100%。如果同時(shí)有4個(gè) IO-base的異步操作,cpu利用率可能遠(yuǎn)遠(yuǎn)低于100%。
對(duì)于.net 庫(kù),有些函數(shù)會(huì)有兩個(gè)版本:一個(gè)是同步操作,一個(gè)是異步操作(函數(shù)名以Async結(jié)尾,返回值為Task)。舉個(gè)例子:

這是WebClient類獲取網(wǎng)址內(nèi)容函數(shù)。你會(huì)問(wèn)DownloadStringTaskAsync是compute-base Task,還是 IO-base Task?我可以肯定的告訴你:只要是.net基本類庫(kù)提供的異步函數(shù)基本都是IO-base Task(微軟官方文檔是這樣要求)。其實(shí)這樣要求是有道理的:對(duì)于compute-base異步,比較容易封裝;再者,這樣的異步是不能大規(guī)模的并發(fā)的。如果16個(gè)線程cpu,同時(shí)并發(fā)16個(gè)這樣的異步操作就是上限了;如果再多,反而會(huì)有害!
有人說(shuō),如果基本類庫(kù)不提供 IO-base Task函數(shù),我也可以封裝一下,這個(gè)也不難??!代碼如下:
//把一個(gè)同步操作,改造成異步
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();
}
});
}
上面函數(shù)如果說(shuō)是異步操作,也不錯(cuò)。但是,這不是“好”的異步操作!這是異步操作中夾雜著同步IO。會(huì)導(dǎo)致線程等待。如果有100個(gè)這樣的異步操作,就需要100個(gè)線程,這些線程大部分并沒(méi)在干活,而是在等待! 對(duì)于“好”的異步IO,如果同時(shí)有100個(gè)操作,甚至幾萬(wàn)個(gè)操作,使用的線程都是有限的,一般不超過(guò)cpu線程數(shù)。這是怎么實(shí)現(xiàn)的?這涉及到IOCP,說(shuō)起來(lái)有些復(fù)雜,可以參考IOCP相關(guān)資料。類庫(kù)提供異步IO操作,都是涉及到IOCP的。所以得到如下結(jié)論: 如果類庫(kù)不提供IO異步函數(shù),無(wú)論怎么改造,不可能改造成“好”的異步函數(shù)!
Task實(shí)現(xiàn)的基本原理
Task變量狀態(tài)如下

狀態(tài)簡(jiǎn)要分為生成、執(zhí)行、執(zhí)行完畢這三個(gè)階段。如果執(zhí)行完畢前獲取執(zhí)行后的值Task.Result,函數(shù)就會(huì)阻塞。那我怎么知道什么時(shí)候完成,而又不阻塞?有兩種辦法,輪詢和回調(diào)通知。Task.IsCompleted屬性會(huì)指示函數(shù)是否執(zhí)行完畢。輪詢不是一個(gè)好的辦法,采用回調(diào)通知是上策!
回調(diào)通知有個(gè)缺點(diǎn):處理邏輯不直觀,回調(diào)函數(shù)與異步調(diào)用函數(shù)不在一塊,還有可能隔著很多行代碼或不在同一個(gè)文件。如果這樣的回調(diào)函數(shù)太多,對(duì)理解代碼邏輯造成困難,代碼不易維護(hù)。微軟也考慮到了這個(gè)問(wèn)題,那就用await關(guān)鍵詞來(lái)解決。await幫你處理了回調(diào)函數(shù)的弊端,其實(shí)await后面的代碼與await前面的代碼不屬于同一個(gè)函數(shù)!await后面的代碼就是回調(diào)函數(shù)!微軟確實(shí)給我們解決了這個(gè)問(wèn)題,但是又帶來(lái)另一個(gè)問(wèn)題。好多人不明白,明明是同一個(gè)函數(shù),怎么實(shí)現(xiàn)了等待而又不阻塞當(dāng)前線程!歸根到底,還是要理解await背后幫你干了啥,否則就會(huì)一直困惑。
要生成Task變量,只要理解幾個(gè)關(guān)鍵的處理步驟就行了。TaskCompletionSource類會(huì)幫助我們生成Task。如果IO完成,設(shè)置Task的狀態(tài)為完成就行了。后面,就會(huì)執(zhí)行回調(diào)函數(shù)(await關(guān)鍵詞幫我干了,你看不到回調(diào))!
如何寫一個(gè)IO-base Task函數(shù)?
大部分情況下不需要自己寫這樣的函數(shù)。但是,人是有好奇心的,如果不明白函數(shù)實(shí)現(xiàn)的原理,總是感覺(jué)不能釋懷!再者,明白函數(shù)實(shí)現(xiàn)原理,就能更好的利用這類函數(shù)。下面講解一下如何利用IOCP來(lái)實(shí)現(xiàn)異步函數(shù)。我沒(méi)有參考.net的源碼,只是根據(jù)邏輯推理應(yīng)該這實(shí)現(xiàn)??隙ê?net源碼實(shí)現(xiàn)有出入,我寫這些代碼主要為了闡明Task實(shí)現(xiàn)原理。
IOCP處理邏輯

對(duì)于IOCP,這里不展開來(lái)講了,否則就跑題了。以socket讀取為例子,簡(jiǎn)單總結(jié)一下:如果你要接收100個(gè)字節(jié)的數(shù)據(jù),你告訴IOCP你要接收100個(gè)字節(jié)數(shù)據(jù),并提供100個(gè)字節(jié)的buffer,函數(shù)立即返回;數(shù)據(jù)到達(dá)后,IOCP通知你,數(shù)據(jù)到了,數(shù)據(jù)就存在你提供的buffer里。
實(shí)現(xiàn)異步IO偽代碼如下:
class AyncInside
{
//完成端口句柄
IntPtr iocpHandle = IntPtr.Zero;
Task<byte[]> ReadFromSocket(int count)
{
//生成此次操作需要相關(guān)數(shù)據(jù)
TaskCompletionSourceRead readInfo = new TaskCompletionSourceRead();
readInfo.Buffer = new byte[count];
//如果沒(méi)生成iocp則生成。
if (iocpHandle == IntPtr.Zero)
{
iocpHandle = CreateIocp();
}
// 告訴iocp,要讀取count字節(jié)數(shù)據(jù)。函數(shù)不會(huì)阻塞,會(huì)立即返回
//從完成端口收到數(shù)據(jù)后,會(huì)調(diào)用ReadScoketCallback
//我們把readInfo也傳給函數(shù)。當(dāng)回調(diào)時(shí),該變量會(huì)傳給回調(diào)函數(shù)。
ReadFromIocp(iocpHandle, readInfo.Buffer, readInfo, ReadScoketCallback);
return readInfo.Tcs.Task;
}
void ReadScoketCallback(byte[] buffer, int readCount,object tag)
{
//tag就是調(diào)用ReadFromIocp時(shí),傳的readInfo
//便于我們知道異步調(diào)用時(shí)的上下文數(shù)據(jù)。
TaskCompletionSourceRead readInfo = tag as TaskCompletionSourceRead;
if(buffer.Length == readCount )
{
//調(diào)用完SetResult后,await后面的代碼就會(huì)被執(zhí)行!
readInfo.Tcs.SetResult(buffer);
}
else if (buffer.Length > 0)
{
Array.Resize(ref buffer, readCount);
readInfo.Tcs.SetResult(buffer);
}
else
{
readInfo.Tcs.TrySetException(new Exception("讀取數(shù)據(jù)異常!socket可能已斷開!"));
}
}
private void ReadFromIocp(IntPtr iocpHandle, byte[] buffer, object tag,
Action<byte[] , int,object> readScoketCallback)
{
throw new NotImplementedException();
}
private IntPtr CreateIocp()
{
throw new NotImplementedException();
}
}
//封裝異步讀取需要的數(shù)據(jù)
class TaskCompletionSourceRead
{
public TaskCompletionSource<byte[]> Tcs { get; set; }
public byte[] Buffer { get; set; }
}
上述代碼與實(shí)際可使用代碼差距還很大,我在這里主要為了闡明原理。通過(guò)上面的代碼,我們可以看到,這個(gè)異步函數(shù)并沒(méi)生成新的線程;網(wǎng)卡驅(qū)動(dòng)和IOCP配合,幫我們接收了數(shù)據(jù)。所以這種方式才是真正可擴(kuò)展的異步IO。
后記
異步IO和可擴(kuò)展服務(wù)緊密關(guān)聯(lián)。對(duì)于.net core平臺(tái),你會(huì)看到很多函數(shù)都是異步的。理解和用好異步IO函數(shù)非常重要。本文通過(guò)自己對(duì)異步IO的理解,試圖通過(guò)代碼闡明異步IO實(shí)現(xiàn)原理。希望你看過(guò)此文后,能對(duì)此有更深的理解!如果此文對(duì)你有所裨益,希望您給點(diǎn)個(gè)贊!
以上就是基于c# Task自己動(dòng)手寫個(gè)異步IO函數(shù)的詳細(xì)內(nèi)容,更多關(guān)于c# Task手寫異步IO函數(shù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#實(shí)現(xiàn)二維數(shù)據(jù)數(shù)組導(dǎo)出到Excel的詳細(xì)過(guò)程
將數(shù)據(jù)庫(kù)查詢出來(lái)的數(shù)據(jù)導(dǎo)出并生成?Excel?文件,是項(xiàng)目中經(jīng)常使用的一項(xiàng)功能,本文將介紹通過(guò)數(shù)據(jù)集生成二維數(shù)據(jù)數(shù)組并導(dǎo)出到?Excel,文中有詳細(xì)的代碼供大家參考,需要的朋友可以參考下2024-09-09
C#與C++之間類型的對(duì)應(yīng)知識(shí)點(diǎn)總結(jié)
這篇文章主要介紹了C#與C++之間類型的對(duì)應(yīng)知識(shí)點(diǎn)總結(jié),對(duì)此有需要的朋友們可以參考下。2019-08-08
C#使用this關(guān)鍵字實(shí)現(xiàn)串聯(lián)構(gòu)造函數(shù)調(diào)用方法
這篇文章主要介紹了C#使用this關(guān)鍵字實(shí)現(xiàn)串聯(lián)構(gòu)造函數(shù)調(diào)用方法,實(shí)例分析了使用this關(guān)鍵字串聯(lián)構(gòu)造函數(shù)的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-01-01
C# WPF開源UI控件庫(kù)MaterialDesign介紹
這篇文章介紹了C# WPF開源UI控件庫(kù)MaterialDesign,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-01-01
C#中類成員的定義的修飾關(guān)鍵詞知識(shí)點(diǎn)總結(jié)
在本篇文章里小編給大家整理了關(guān)于C#中類成員的定義的修飾關(guān)鍵詞知識(shí)點(diǎn)內(nèi)容,有需要的朋友們可以參考下。2020-02-02
C#實(shí)現(xiàn)Windows服務(wù)安裝卸載開啟停止
本文主要介紹了C#實(shí)現(xiàn)Windows服務(wù)安裝卸載開啟停止,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
RegexOptions.IgnoreCase正則表達(dá)式替換,忽略大小寫
RegexOptions.IgnoreCase正則表達(dá)式替換,忽略大小寫,需要的朋友可以參考一下2013-03-03

