進程:運行應用程式實例 線程:對CPU進行虛擬化。windows為每個進程都提供了該進程專用的線程,這樣當一個進程“假死”,不會影響到其他進程。 線程開銷 線程可以使windows在長時間運行任務可以隨時得到響應,允許用戶使用一個應用程式將已凍結的應用程式強制結束。 線程內核對象(thread ke ...
進程:運行應用程式實例
線程:對CPU進行虛擬化。windows為每個進程都提供了該進程專用的線程,這樣當一個進程“假死”,不會影響到其他進程。
線程開銷
線程可以使windows在長時間運行任務可以隨時得到響應,允許用戶使用一個應用程式將已凍結的應用程式強制結束。
- 線程內核對象(thread kernel object)
操作系統為系統中創建的每個線程都分配並初始化的數據結構之一。該數據結構包含了線程上下文(線程上下文實際上是記憶體塊,中包含了CPU指令集) - 線程環境塊(thread environment block,TEB)
在用戶模式中分配和初始化的記憶體塊。TEB耗用一個記憶體頁(x86和x64 CPU是4K,IA64 CPU是8k) - 用戶模式棧(user-mode stack)
用於存儲傳給方法的局部變數和實參。它還包含一個方法返回時線程從什麼地方開始執行的地址。windows預設為用戶模式棧分配1M記憶體 - 內核模式棧(kernel-mode stack)
出於安全原因,針對從用戶模式的代碼傳遞給內核的任何實參,Windows會把它們從線程的用戶模式棧複製到線程的內核模式棧。一經複製,內核就可驗證實參的值。應用程式無法訪問內核模式棧,所以驗證後的值是無法修改的。 - DLL線程連接(attach)和線程分離(detach)通知
任何時候在進程中創建線程或線程終止,都會調用DllMain方法(創建傳遞DLL_THREAD_ATTACH標誌,終止傳遞DLL_THREAD_DETACH標記)
上下文切換
單CPU電腦一次只能做一件事,所以Windows必須在系統中的所有線程(邏輯CPU)之間共用物理CPU。在任何一刻,Windows只將線程分配給一個CPU,該線程允許運行一個“時間片”(也稱為“量”或者"量程",即quantum)。一旦時間片到期,Windows的上下文就切換到另個一個線程。
- 將CPU寄存器中的值保存到當前正在運行的線程的內核對象內部的一個上下問的結構中
- 從現有線程集中選出一個線程供調度,如果該線程由另一個進程擁有,Windows開始執行代碼或接觸任何數據之前,還必須切換CPU“看見”的虛擬地址空間
- 將所選上下文結構中的值載入到CPU寄存器中
Windows大約30毫秒執行一次上下文切換。上下文切換是凈開銷(即開銷不會換來任何性能的提升)。當一個應用程式的線程進入無限迴圈,Windows會頂起搶占(preempt)它,將一個不同線程分配給CPU,讓新的線程運行一會。
上下文切換性能影響
CPU要執行一個不同的線程,而之前的線程的代碼和數據還在CPU的緩存中,這使CPU不必經常訪問RAM(RAM的速度要比CPU的高速緩存慢得多),當Windows上下文切換到一個新的線程時,這個新的線程極有可能要執行不同的代碼並訪問不同的數據,這些代碼和數據並不在CPU的高速緩存中,因此,CPU必須訪問RAM來填充它的告訴緩存,以恢復高速執行的狀態,但30毫秒後,新的一輪上下文切換又開始了。
切換的所需的時間是由CPU架構和速度決定的。而填充CPU緩存的時間取決於系統中運行的程式、CPU緩存大小等因素。所以無法為每次上下文切換的時間開銷確定一個值。如果要構建高性能的應用程式和組件,應儘可能的避免上下文切換。
一個時間片結束,如果Windows決定再次調度同一線程(而不是切換到另一線程),那麼Windows不會執行上下文切換。設計代碼時,上下文切換能避免儘量避免。雖然說避免使用線程(會造成消耗大量記憶體,浪費時間創建、銷毀和管理線程),但使用線程也使Windows變得更加穩定。
安裝多個CPU(或多核CPU)的電腦可以真正實現同時運行多個線程,這提升了應用程式的可伸縮性(併發執行多任務)。
資源浪費
在Windows中,相對於線程的創建,創建一個進程通常需要幾秒,必須分配大量的記憶體(從磁碟中載入DLL和exe文件等等操作),所以一般開發人員都選擇創建線程。這就是為什麼我們的電腦上一般只開了幾十個程式(進程),但是有上千個線程的原因。雖然線程比進程廉價,但是和其他系統資源相比忍讓非常昂貴。之前我們已經算過,創建一個線程至少1M多,如果1000個線程就是1G多。打開任務管理器,我們可以看到很多程式暫用了大量的記憶體,但是他們的CPU使用率幾乎為0%。
創建一個專用的線程
創建線程執行非同步操作,一般推薦通過CLR的線程池來執行操作。但是滿足以下幾點的話,也可以顯示創建一個線程來執行非同步操作。
- 線程需要非普通線程優先順序運行;線程池的所有線程都是以普通優先順序運行。
- 需要線程表現為一個前臺線程,防止應用程式線上程結束它的任務前終止。
- 需要長時間運行
- 要啟動一個線程,並可能調用Thread的Abort方法來提前終止它
System.Threading.Thread thread = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart((o) =>
{
System.Threading.Thread.Sleep(500);
Console.WriteLine("Hello 線程1");
}));
thread.Start();
Console.WriteLine("Hello 線程2");
使用線程的理由
- 可以使用線程代碼同其他代碼隔離
- 可以使用線程簡化編碼
- 可以用線程來實現併發執行
今天,CPU的成本越來越低,電腦使用的CPU核心越來越多;所以軟體應該大膽的去利用硬體,比如Visual Studio的編輯器停止編輯的時候,Visual Studio會自動編譯代碼,這樣極大的提高了開發著的效率,傳統的“編輯代碼-編譯生成-調試”模式逐漸被“編輯代碼-調試”模式取代,因為有大量CPU能力提供使用,編譯器的頻繁運行不會影響到其他事情。
線程調度和優先順序
Windows之所以被稱為一種搶占式多線程操作系統,是因為線程可以在任何時間被停止(搶占),並調度另一個線程。用戶有一定的控制權,但是並不能保證自己的線程一直運行,並且不能阻止其他線程運行。
線程的優先順序從0(最低)~31(最高),系統決定將哪一個線程分配給CPU時,先檢查優先順序31的線程,並以輪流的方式調用他們。只要存在可調度的優先順序為31的線程,系統就永遠不會將優先順序0~30的線程分配給CPU,這種情況稱為饑餓(starvation)。
系統啟動時,會創建優先順序為0的零頁線程,零頁線程的作用是沒有其他進程需要執行的時候將系統RAM空閑頁清零。
在設計程式時,應決定自己的應用程式是比機器上運行的其他應用程式更大還是更小的影響力。然後選擇一個優先順序類,Windows支持6個進程優先順序類:Idle
,Below Normal
,Normal
,Above Noraml
,High
,Reatime
。進程的優先順序類預設值一般為Noraml
。只有絕對必要的時候才使用High
優先順序,特別是Reatime
,等級越高會直接影響到操作系統的某些任務。
Windows支持7個相對線程優先順序:Idle
,Lowest
,Below Normal
,Normal
,Above Noraml
,Highest
,Reatime-Critical
。這些優先順序是相對於進程優先順序類的。可以將進程看作是優先順序類的成員,在進程中要為線程分配優先順序。
前臺線程和後臺線程
CLR將每個線程要麼視為前臺線程,要麼是後臺線程。一個進程的所有前臺線程停止運行時,CLR強制終止仍然在運行的後臺線程(且不會拋出異常)。
在一個線程的生存期中,任何時候都可以由前臺線程變為後臺線程,或者從後臺變為前臺。應用程式的主線程和通過構造一個Thread對象顯示創建的任何線程預設為前臺線程,線程池線程預設為後臺線程。
推薦使用CLR的線程池,它自動可以管理線程的創建、銷毀。並重用已創建的線程,避免了不必要的開銷。