在當初試用多線程的時候發現多線程能減輕或消除大量繁雜操作或過長等待時間造成的停滯感(就是線程阻塞)。后來發現使用異步操作也能達到相同的效果。但是兩者之間是有區別的,之前在知識庫里看了一些文章,我也記錄了一下(有人雲亦雲的感覺),順便也擺出一些個人觀點。
多線程和異步雖然都可以減輕或消除線程阻塞而造成的停滯感,但是兩者的本質上是有區別的
多線程是軟件級別上的機制,在微觀上它是分配CPU的時間片給某個進程中的各條線程,獲得時間片的線程就可以處理它的任務,也就是執行代碼。在其中負責調度CPU資源的就是操作系統,所以多線程是否能實現取決於操作系統,現今絕大部分操作系統都是多線程的系統,在DOS下是不支持多線程的。
異步則是硬件級別上的機制,在大學學習《計算機組成原理》時,就提過硬件的DMA(Direct Memory Access,直接內存存取),它是讓一些計算機的外部設備(網卡,磁盤等)在不用消耗CPU時間的情況下,直接與內存交互進行數據讀寫,在此期間CPU可以着手其他事情,在IO完畢后才把調度權還給CPU。由此可見,過中不需要操作系統的支持,所以按照這樣的思路去想,只需要計算機的硬件支持的話,在DOS中也能實現異步操作(這個在網上看到的,實際上我也沒搞個DOS去實踐)。
由上述的區別可以推斷出他們的適用場合,異步是硬件層面的,一些關於硬件層面上的操作用起異步來會適合一些,例如文件的讀寫操作,數據庫訪問,網絡訪問等;多線程是軟件層面的,CPU是否把時間片分配給當前線程,與能否讓外部設備直接訪問內存關系不大,相同的操作也是同樣要消耗相同的CPU時間,相比起來,一些要CPU花費大量時間去處理的操作用多線程去實現會恰當一點。
理論上就如前面所說的,但是回到.NET Framework里面,貌似是另一回事了。通過運行以下代碼
1 static void Main(string[] args) 2 { 3 PrintThreadInfo("Main"); 4 Action act = delegate() { PrintThreadInfo("Action"); }; 5 IAsyncResult ir = act.BeginInvoke(new AsyncCallback(AsyncCallback), act); 6 7 Console.ReadLine(); 8 } 9 10 static void AsyncCallback(IAsyncResult result) 11 { 12 PrintThreadInfo("Callback"); 13 Action caller = result.AsyncState as Action; 14 caller.EndInvoke(result); 15 } 16 17 static void PrintThreadInfo( string host ) 18 { 19 Console.WriteLine(string.Format( " {2} call: Current Thread Id is {0}, it {1} in threadpool",Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread?"is":"isn't",host )); 20 }
發現異步操作實際上還是利用了多線程,而且這條新開辟的線程是來源於線程池ThreadPool的。在MSDN上翻找了一下BeginInvoke的解釋的確是異步調用的。它確確實實屬於APM模式(BeginXXX/EndXXX)。那個再觀察一下別的類在APM模式下的工作情況
1 static void Main(string[] args) 2 { 3 PrintThreadInfo("Main"); 4 byte[] datas = Encoding.ASCII.GetBytes("hello world"); 5 FileStream fs = new FileStream("abc.txt", FileMode.Create, FileAccess.Write,FileShare.Write, 1024, FileOptions.Asynchronous); 6 fs.BeginWrite(datas, 0, datas.Length, new System.AsyncCallback(AsyncCallback), fs); 7 8 9 Console.ReadLine(); 10 } 11 12 static void AsyncCallback(IAsyncResult result) 13 { 14 PrintThreadInfo("Callback"); 15 FileStream caller = result.AsyncState as FileStream; 16 caller.EndWrite(result); 17 caller.Close(); 18 caller.Dispose(); 19 }
這里選取了FileStream作例子,但從結果可以看出,縱使確確實實是異步操作,確確實實是文件寫入,但是仍然是有調用了線程池,使用了線程。看回第一個例子的結果BeginInvoke和回調方法都是在同一條線程上執行的,相比起第二個例子就有個局限性,在BeginWrite調用的時候沒辦法看查看是否有使用線程去進行寫操作,第二行信息是在回調時顯示出來的。那這里是否和上一個例子一樣兩者都在同一個線程上運行呢?我有個比較拙劣的辦法如下圖所示
第一個例子中的情況
第二個例子中的情況
雖然這樣斷點測試貌似有點誤差,不知有否說服力。對比之下還是可以看出第一個例子它調用異步的時候就創建了線程,嚴格意義上並不屬於異步,第二個例子調用異步方法時沒有創建線程,直到回傳的時候才去創建了線程。可以初步證實在調用BeginWrite的時候並沒有去創建線程,確實是使用了DMA機制,確確實實是異步調用了。
參考趙劼老師說的話,CLR會(通過Windows API)發出一個IRP(I/O Request Packet)。當設備准備妥當,就會找出一個它“最想處理”的IRP(例如一個讀取離當前磁頭最近的數據的請求)並進行處理,處理完畢后設備將會(通過Windows)交還一個表示工作完成的IRP。CLR會為每個進程創建一個IOCP(I/O Completion Port)並和Windows操作系統一起維護。IOCP中一旦被放入表示完成的IRP之后(通過內部的ThreadPool.BindHandle完成),CLR就會盡快分配一個可用的線程用於繼續接下去的任務。
個人理解就是CLR與底層硬件交互,讓相應設備的不再消耗CPU去設備訪存。正如文章開頭的理論部分所言,異步屬於硬件方面的,所以一些委托的異步調用BeginInvoke並是假異步,例如上面文件操作的異步才是真異步,能實現真異步的有以下方法
- FileStream操作:BeginRead、BeginWrite(只有構造FileStream時傳入FileOptions.Asynchronous參數才能獲取真正的異步操作,否則仍然是假異步)。
- DNS操作:BeginGetHostByName、BeginResolve。
- Socket操作:BeginAccept、BeginConnect、BeginReceive等等。
- WebRequest操作:BeginGetRequestStream、BeginGetResponse。
- SqlCommand操作:BeginExecuteReader、BeginExecuteNonQuery等等(要在連接字符串中把Asynchronous Processing設為true,否則調用異步方法時會拋異常)。
- WebServcie調用操作:例如.NET 2.0或WCF生成的Web Service Proxy中的BeginXXX方法、WCF中ClientBase<TChannel>的InvokeAsync方法。
最后要記錄一下的就是使用APM模式的時候一定要調用回調方法或者EndXXX,否則線程資源無法回收,有可能導致系統崩潰。
以上文章有參考趙劼老師的《正確使用異步操作》,還有一部分是在下的拙見,各位覺得在下有什么說錯的歡迎批評指正,有什么建議或意見盡管說說。謝謝!