線程機制、CLR線程池以及應用程序域
線程機制、CLR線程池以及應用程序域
最近在總結多線程、CLR線程池以及TPL編程實踐,重讀一遍CLR via C#,比剛上班的時候收獲還是很大的。還得要多讀書,讀好書,同時要多總結,多實踐,把技術研究透,使用好。
話不多說,直接上博文吧。先說一下,為什麽Windows要支持線程機制?
1. Windows為什麽要支持線程
計算機的早期時代,操作系統沒有線程的概念,整個系統只運行著一個執行線程,其中包含操作系統代碼和應用程序代碼。只用一個執行線程的問題在於,長時間運行的任務會阻止其他任務的執行。例如16位Windows的時代,打印文檔的應用程序很容易“凍結”整個機器。
Microsoft 在設計Windows NT這個版本的OS內核時,決定在一個進程中運行應用程序的每個實例。進程實際是應用程序的實例要使用的資源的集合。每個進程都被賦予了一個虛擬地址空間,確保一個進程中使用的代碼和數據無法由另一個進程訪問。這就確保了應用程序實例的健壯性。同時,進程訪問不了OS的內核代碼和數據;所以,應用程序代碼破壞不了操作系統的代碼和數據
如果應用程序發生死循環會發生什麽?如果機器只有一個CPU,它會執行死循環,不能執行其他任何程序。Microsoft 的解決方案就是線程。作為一個Windows概念,線程的職責是對CPU進行虛擬化。Windows為每個進程都提供了該進程專用的線程(功能相當於一個CPU)。應用程序的代碼進行死循環,與代碼關聯的進程會“凍結”,但其他進程(它們有自己的線程)不會凍結,它們會繼續執行。
線程很強大,因為它們使Windows即使在執行長時間運行的任務時,也能隨時響應。
但是,和一切虛擬化機制一樣,線程有空間(內存消耗)和時間(運行時的執行性能)的開銷。
2. 線程開銷有哪些?
每個線程都有以下要素組成:
線程內核對象(thread kernel object):線程的描述屬性和線程上下文,上下文是包含CPU寄存器集合內存塊。對於x86、x64、ARM CPU架構,線程上下文分別使用約700,1240和350字節的內存。
線程環境塊(thread environment block,TEB):用戶模式(應用程序代碼能快速訪問的地址空間)中分配和初始化的內存塊。TEB耗用一個內存頁( x86、x64、ARM CPU 中是4KB)
用戶模式棧(user-mode stack):用戶模式棧存儲傳給方法的局部變量和實參,它還包含一個地址:指出當前方法返回時,線程應該從什麽地方接著執行。Windows默認為每個線程的用戶模式棧分配1MB內存。Windows只是保留1MB地址空間,在線程實際需要時才會提交(調撥)物理內存。
內核模式棧(kernel-mode stack):應用程序代碼向操作系統中的內核模式傳遞參數時,還會使用內核模式棧,出於安全的考慮,Windowd會把這些實參從線程的用戶模式棧復制到線程的內核模式棧。32windows 內核模式棧大小12KB,64位是24KB
DLL線程連接(Attach)和線程分離(Detach)通知:Windows的一個策略是,任何時候在進程中創建線程,都會調用進程中加載的所有非托管DLL的DllMain方法,並向該方法傳遞DLL_THREAD_ATTACH標誌。同樣的,任何時候線程終止,都會調用進程中的所有非托管DLL的DllMain方法,並向該方法傳遞DLL_THREAD_DETACH標誌。
上下文切換
單CPU計算機一次只能做一件事情。所以,Windows必須在操作系統中的所有線程(邏輯CPU)之間共享物理CPU。
Windows任何時刻只將一個線程分配給一個CPU。那個線程能運行一個“時間片”的長度。時間片到期,Windows就將上下文切換到另一個線程。每次上下文切換都要求Windows執行一下操作:
- 將CPU寄存器的值保存到當前正在運行的線程的內核內部的一個上下文結構中
- 從現有線程集合中選出一個線程供調度
- 將所選上下文結構中的值加載到CPU寄存器中
當Windows上下文切換到另一個線程時,會產生一定的性能損失。
一個時間片結束時,如果Windows決定再次調度同一個線程(而不是切換到另一個線程),那麽Windows不會執行上下文切換。
3. 使用線程的理由:什麽場景下使用線程
可響應性(通常是針對客戶端GUI應用程序):客戶端GUI應用程序中,可以將一些工作交給一個線程進行,使GUI線程能靈敏的響應用戶的輸入。在這個過程中創建的線程數可能超過CPU的核心數,會浪費系統資源和降低性能。但是用戶體驗得到改善和增強。
性能(對於客戶端和服務端應用程序):由於Windows每個CPU調度一個線程,而且多個CPU能並發執行這些線程,所以同時執行多個操作可以提升性能。
4. CLR線程池機制
創建和銷毀線程是一個昂貴的操作,要耗費大量的時間,同時,太多的線程會浪費內存資源。由於操作系統必須調度可運行的線程並執行上下文切換,所以太多的線程會影響性能。
為了改善這個情況,CLR提供了線程池機制,每個CLR一個線程池。
CLR線程池並不會在CLR初始化的時候立刻建立線程,而是在應用程序要創建線程來執行任務時,線程池才初始化一個線程。線程的初始化與其他的線程一樣。在完成任務以後,該線程不會自行銷毀,而是以掛起的狀態返回到線程池。直到應用程序再次向線程池發出請
求時,線程池裏掛起的線程就會再度激活執行任務。
這樣既節省了創建線程所造成的性能損耗,也可以讓多個任務反復重用同一線程,從而在應用程序生存期內節約大量開銷。
5. 進程、線程和應用程序域
進程(Process):Windows系統中的一個基本概念,它包含著一個運行程序所需要的資源。進程之間是相對獨立的,一個進程無法訪問另一個進程的數據(除非利用分布式計算方式),一個進程運行的失敗也不會影響其他進程的運行,Windows系統就是利用進程把工作劃分為多個獨立的區域的。進程可以理解為一個程序的基本邊界。
應用程序域(AppDomain):一組程序集的邏輯容器。CLR在初始化在初始化時創建第一個AppDomain(默認AppDomain),這個AppDomain在進程終止時被銷毀。.NET的程序集正是在應用程序域中運行的。
一個進程可以包含有多個應用程序域,一個應用程序域也可以包含多個程序集。
在一個應用程序域中包含了一個或多個上下文context,使用上下文CLR就能夠把某些特殊對象的狀態放置在不同容器當中
線程(Thread):進程中的基本執行單元,在進程入口執行的第一個線程被視為這個進程的主線程。在.NET應用程序中,都是以Main()方法作為入口的,當調用此方法時系統就會自動創建一個主線程。
線程主要是由CPU寄存器、調用棧和線程本地存儲器(Thread Local Storage,TLS)組成的。CPU寄存器主要記錄當前所執行線程的狀態,調用棧主要用於維護線程所調用到的內存與數據,TLS主要用於存放線程的狀態信息。
6. 進程、線程和應用程序域的關系
進程、應用程序域、線程的關系如下圖,
一個進程內可以包括多個應用程序域,也有包括多個線程,線程也可以穿梭於多個應用程序域當中。但在同一個時刻,線程只會處於一個應用程序域內。
線程機制、CLR線程池以及應用程序域