基於c# Task自己動手寫個異步IO函數

前言    

對於服務端,達到高性能、高擴展離不開異步。對於客戶端,函數執行時間是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實現原理。希望你看過此文後,能對此有更深的理解!如果此文對你有所裨益,希望您給點個贊!

以上就是基於c# Task自己動手寫個異步IO函數的詳細內容,更多關於c# Task手寫異步IO函數的資料請關註WalkonNet其它相關文章!

推薦閱讀: