.NET多線程和異步總結(一)

前言

本文源於筆者在公司內部的一個分享。幾月前爲了搞懂這些知識花費了大量的時間調查研究,最終的理解算是全面而透徹了。而如今學習其餘技術時,間或會遇到與此相似的話題,因而把先前的總結記錄下來,以做備忘,並啓發本身舉一反三。文中圖片都取自當時的Slides。數據庫

爲什麼要關注多線程和異步

服務器的計算分爲IO計算和CPU計算。IO計算指計算任務中以IO爲主的計算模型,好比文件服務器、郵件服務器等,混合了大量的網絡IO和文件IO;CPU計算指計算任務中沒有或不多有IO,好比加密/解密,編碼/解碼,數學計算等等。編程

須要關心的是IO計算,通常的網絡服務器程序每每伴隨着大量的IO計算。提升性能的途徑在於要避免等待IO 的結束,形成CPU空閒,要儘可能利用硬件能力,讓一個或多個IO設備與CPU併發執行。服務器

另外一方面,CPU密集的計算是咱們沒法控制的。若是是CPU計算出現了瓶頸,那隻能給服務器增長CPU,或者增長服務器。而IO操做,其實是空等別的硬件,這裏面的優化就大有可爲。網絡

大部分Web服務的大部分操做都是IO密集型的。無非是讀磁盤、查數據庫、訪問網絡調用別的API,而這些都是經過操做系統的IO。

多線程是必須的,但線程並不是越多越好

爲了下降請求等待時間,對每一個請求都創建一個線程來處理能夠嗎?這是不少早期服務器的方式。而這種方式存在很大的問題。多線程

首先,建立線程/進程和銷燬線程/進程的代價很是高昂,尤爲是在服務器採用TCP「短鏈接」方式或UDP方式通信的狀況下,例如,HTTP協議中,客戶端發起一個鏈接後,發送一個請求,服務器迴應了這個請求後,鏈接也就被關閉了。若是採用經典方式設計HTTP服務器,那麼過於頻繁地建立線程/銷燬線程對性能形成的影響是很惡劣的。併發

線程還會佔用內存。線程的內核對象佔幾M到幾時M。普通的計算機上千個線程就會耗盡內存。app

線程切換也會消耗時間,最終致使服務器性能急劇降低。若是客戶端併發請求量很高,同一時刻有不少客戶端等待服務器響應的狀況下,將會有過多的線程併發執行,頻繁的線程切換將用掉一部分計算能力。負載均衡

對於一個須要應付同時有大量客戶端併發請求的網絡服務器來講,線程池是惟一的解決方案。線程池不光可以避免頻繁地建立線程和銷燬線程,並且可以用數目不多的線程就能夠處理大量客戶端併發請求。框架

異步IO是關鍵

只有線程池,而繼續同步等待IO請求等於沒用,可達到的吞吐量將很是有限。異步IO則可讓線程池中的每一個線程用很短的時間處理請求中的計算任務,而後把任務交給IO,線程繼續處理別的請求。當IO返回時,一個線程池中的線程再被指派來完成剩下的工做,直到完成請求的響應。如此一來,就能夠壓榨出最大的硬件性能。
這也是爲什麼Nodejs將異步做爲核心賣點,各大主流語言和框架都有相似的異步支持和async/await句式了。異步

從IO講起

Windows IO的基本過程以下:

clipboard.png

其中要用到中斷和APC,簡要介紹一下:

中斷

CPU在執行指令的間隙能夠被中斷打斷,進入內核態。內核態共享一個地址空間,沒有進程線程之分。CPU將執行中斷分發例程,保存執行現場,執行中斷服務例程,再恢復現場,繼續執行原來的的指令。

clipboard.png

中斷還有優先級的概念。處於高優先級時,低優先級的中斷不能被處理,直到CPU中斷優先級降下來。

clipboard.png

APC

是操做系統提供的一種異步回調機制。能夠把一段任務代碼放到某個用戶線程的內核結構的某個隊列中,程序正常運行時不會執行。只有當線程發起某些調用,使本身成爲Alertable(可喚醒)時,纔會檢查APC隊列,把其中的任務都執行了。這些進入可喚醒狀態的調用有:

  • WaitForSingleObjectEx()
  • SleepEx() 等等。

再來看第一幅圖。經過SYSCALL指令進行中斷調用,進入內核態。內核調用驅動程序去完成IO。而調用是否當即返回(異步),仍是阻塞等待(同步),取決於傳入的參數。另外一張.NET IO的圖則能夠提供更多理解。

clipboard.png

Windows IO的幾種模式

  1. 同步模式

    • 調用 CreateFile(), ReadFile(), WriteFile()等API的默認方式就是同步阻塞。
  2. 異步模式

    • Overlapped結構體

      • CreateFile(..., FILE_FLAG_OVERLAPPED,...) 打開文件時加入此標誌啓動異步模式,API調用會當即返回。
      • 使用GetOverlappedResult/WaitForMultipleObjects等待Overlapped結構體,能夠等待IO完成。
    • APC完成例程

      • 如此調用WriteFile(..., lpCompletionRoutine),傳入一個IO完成例程,內核會在IO完成時將此例程放入線程APC隊列。此線程再在適時調用SleepEx()等進入可喚醒狀態,執行IO完成例程。
    • IOCP(IO 完成端口)

      • CreateIoCompletionPort()建立IO完成端口。
      • 調用GetQueuedCompletionStatus()等待IO完成端口上面的IO完成信號。IO完成端口會保證只觸發合適數量的線程(約等於CPU核心數)來處理IO完成後的工做。
      • 調用PostQueuedCompletionStatus()來關閉IO完成端口。

以上講解對於不瞭解Windows核心編程的讀者來講確定沒法造成什麼認識。如下經過一個例子來介紹這幾種模式。

一個Socket服務器的示例

這裏用一個Winsock2(Socket的Windows版)的程序舉例。只做圖示,具體代碼未必徹底契合。
紅色表示阻塞。

  1. 同步模型。單線程,一次只能處理一個請求。

    clipboard.png

  2. 多線程。每次請求都另起線程處理,能同時處理多個請求了。可是線程也越變越多,抗不住。

    clipboard.png

  3. 使用Overlapped結構體實現異步。只有一個線程,調用GetOverlappedResult()等待多個Overlapped結構,有完成的IO就解決處理,而後繼續等待。一個線程就能夠處理全部請求!

    clipboard.png
    惋惜它也有缺點。等待多個對象相對於WaitForMultipleObjects(),而這個函數有64個等待對象的上限。因此只能用多線程來等待。當請求量很大時,線程數量也會變大。

  4. 使用APC。一樣是一個線程,在調用IO函數時傳入回調,而後不停地進入可喚醒態,等待IO完成後觸發APC回調。

    clipboard.png
    缺點:線程執行的主動性不在本身;負載均衡不易控制。現在已極少使用。

  5. 使用IOCP。Windows的大殺器。IOCP能夠激活合適數量的線程來執行IO完成後的任務。

    clipboard.png 能夠把IOCP理解成一種內核同步機制。

相關文章
相關標籤/搜索