【C#進階系列】25 線程基礎


線程的概念

線程的職責是對CPU進行虛擬化。

CPU為每個進程都提供了該進程專用的線程(功能相當於cpu),應用程序如果進入死循環,那么所處的進程會"凍結",但其他進程不會凍結,它們會繼續執行!

線程的開銷

因為是虛擬化CPU,所以也會有空間(內存耗用)和時間(執行性能)上的開銷。

具體的開銷:

  • 線程內核對象(thread kernel object)
    • 操作系統為創建的每個線程都會分配並初始化這種數據結構。數據結構包含一組對線程進行描述的屬性,還包含線程的上下文,包含模擬CPU寄存器的集合的內存塊。
  • 線程環境塊(thread environment block,TEB)
    • TEB是在用戶模式下分配和初始化的內存塊。
    • TEB包含線程的異常處理鏈首。線程進入每個try塊都在鏈首插入一個節點,線程退出try塊從鏈中刪除該節點。
    • TEB還包含線程的“線程本地存儲”數據,以及由GDI和OpenGL圖形使用的一些數據結構。
  • 用戶模式棧(user-mode stack)
    • 用戶模式棧存儲傳給方法的局部變量和實參。包含一個地址,指出當前方法返回,線程應該從什么地方接着執行。
    • Windows默認為每個線程的用戶模式棧分配1MB的空間。 
  • 內核模式棧(kernel-mode stack)
    • 應用程序代碼向操作系統中的內核模式函數傳遞實參時,還會使用內核模式棧。針對傳給內核的任何實參,都會從用戶模式棧復制到內核模式棧。
  • DLL線程連接(attach)和線程分離(detach)通知
    • 任何時候在進程中創建線程,都會調用進程中加載的所有非托管DLL的DLLMain方法,並向該方法傳遞DLL_THREAD_ATTACH標志。
    • 任何時候在進程中線程終止,都會調用進程中加載的所有非托管DLL的DLLMain方法,並向該方法傳遞DLL_THREAD_DETACH標志

從上面這些開銷可以看出,創建和銷毀一個線程的開銷雖然沒有進程那么大,但是也不小了。

甚至減少線程的數量還會提高垃圾回收的性能,因為垃圾回收時會掛起所有線程。

線程的切換也會有性能損失:

單CPU計算機一次只能做一件事情,所以所有的線程實際上是共享物理CPU的。多個CPU的計算機或者多核CPU,可以真正同時運行幾個線程。然而單個線程還是只能在一個內核上運行。

任何時刻,一個CPU都只會被分配給一個線程。那個線程占用CPU一段時間后(叫做時間片),就會切換到另一個線程。(如果時間片結束后,Windows決定再次調用同一線程,那么不會執行線程切換)

線程切換大概每30毫秒進行一次。

每次線程切換時進行的操作:

  1. 將CPU寄存器的值保存到當前正在運行的線程的線程內核對象(前面提到過)內部的一個上下文結構。
  2. 從現有線程集合中選出一個線程供調度。如果該線程由另一個進程擁有,Windows在開始執行任何代碼或者接觸任何數據之前,還必須切換CPU“看見”的虛擬地址空間。
  3. 將切換到的新線程中,線程內核對象中的模擬寄存器的值加載到CPU的寄存器中

線程切換雖然消耗性能,但是卻提供了一個健壯靈敏的操作系統。如果某線程進入死循環,不會影響其它線程。

線程在等待IO操作,會使線程進入等待狀態,讓線程在任何CPU上都不再調度,直到發生下一次輸入事件。

使用專用線程執行異步的計算限制操作

以下介紹使用專用線程執行異步的計算限制操作,但是建議避免使用此技術,而用線程池來執行異步的計算限制操作。

如果執行的代碼要求線程處於一種特定的狀態,而這種狀態對於線程池線程來說是非同尋常的,就可以考慮創建專用線程。

例如:

  • 線程需要以非普通線程優先級運行。而所有線程池都以普通優先級運行;雖然可以更改這一優先級,但不建議這么做。並且在不同的線程池操作之間,優先級的更改是無法持續的
  • 需要線程表現為一個前台線程,防止應用程序在線程任務結束前終止。線程池現場呢個始終為后台線程。如果CLR想終止進程,那么它們就完成不了任務。
  • 計算限制的任務需要長時間運行。線程池為了判斷是否需要創建一個額外的線程,所采用的邏輯是比較復雜的。直接為長時間運行的任務創建專用線程,就可以避免這一問題。
  • 要啟動線程,並可能調用Thread的Abort方法來提前終止它。

一個簡單的使用專用線程執行異步操作的例子:

     static void Main(string[] args)
{
Thread 某線程
= new Thread(線程回調函數);
某線程.Start(
"hello");
Console.WriteLine(
"某線程運行開始");
某線程.Join();
//join方法造成調用線程阻塞當前執行的任何代碼,直到“某線程”銷毀或者終止
Console.WriteLine("繼續運行");
Console.Read();
}
private static void 線程回調函數(Object 狀態參數) {
Thread.Sleep(
10000);
if (狀態參數.GetType() == typeof(string))
{
Console.WriteLine(
"這是一個字符串");
}
else {
Console.WriteLine(
"未識別");
}
}

使用線程的理由

  • 可響應性
    • 在客戶端GUI應用程序中,可以將一些工作交給線程進行,使GUI線程能靈敏響應用戶輸入。
  • 性能
    • 在多個CPU或多核CPU上使用多線程會提升性能,因為它可以真正意義上同時執行多件事情

線程調度和優先級

搶占式操作系統必須使用算法去判斷什么時候調度哪些線程多長時間。

前面提到過,每個線程都包含一個線程內核對象,而內核對象中包含一個上下文結構,此結構中存儲了線程上一次執行完畢后CPU寄存器的狀態。

在一個時間片完后,windows會檢查現存的所有線程內核對象,在這些對象中,只有那些沒有正在等待什么的線程才適合調度。

Windows選擇一個可調度的線程內核對象,並上下文切換到它。

然后線程開始執行代碼,並在其進程的地址空間處理數據。然后過了一個時間片完后又循環執行以上操作。

Windows從系統啟動開始便一直執行上下文切換,直到系統關閉為止。

之所以被稱為搶占式操作系統,是因為線程可以在任何時間停止(被搶占)並調度另一個線程。

 

每個線程都分配了從0(最低)到31(最高)的優先級。系統決定為CPU分配哪個線程時,首先檢查優先級為31的線程,並以一種輪流的方式調度它們。

只要還存在優先級高的可調度線程,那么就不會將優先級低的線程分配給CPU。這種情況稱為飢餓。

系統啟動時會創建一個特殊的零頁線程,其優先級為0,而且是整個系統中唯一優先級為0的線程。在沒有其它線程需要工作的時候,零頁線程會將系統RAM的所有空閑頁清零。

 

將優先級設為數字,實際操作中很難分配合理,於是微軟給了一個更簡單的方法。

在設計應用程序時,可以選擇一個進程優先級類(可以選擇Idle,Below Normal,Normal,Above Normal,High和Realtime),默認的Normal為最常見的優先級類。

事實上進程優先級類只是一個抽象的概念,Windows永遠不會調度進程,只會調度線程。

在系統中什么都不做的時候運行的應用程序如屏保程序適合分配Idle優先級類。

而RealTime優先級優先級太高,甚至可能影響到操作系統任務,可能造成不能及時地處理鍵盤和鼠標輸入。

而Windows還支持7個相對線程優先級(Idle,Lowest,Below Normal,Normal,Above Normal,Highest和Time-Critical),它們和進程優先級類一起確定最后的線程優先級。

最好是降低一個應用程序的優先級而不是提高另一個線程的優先級。高優先級的線程大多數時候應該使其保持為等待狀態。

而我們可以通過設置Thread的Priority,向其傳送ThreadPriority枚舉類型定義的5個值之一:Lowest,Below Normal,Normal,Above Normal,Highest。

沒有Idle和Time-Critical是因為CLR保留了。之前在垃圾回收那里提到的CLR的終結器線程以Time-Critical優先級運行。

如果應用程序以特殊的安全權限運行,可以使用System.Diagnostics命名空間中的Process和ProcessThread類,這兩個類分別提供了進程和線程的Windows視圖。

應用程序也可以使用AppDomain和Thread類,它們公開了AppDomain和線程的CLR視圖。(這兩個類不需要特殊的安全權限,只有部分操作需要)

前台線程和后台線程

CLR將線程分為前台線程和后台線程。

一個進程的所有前台線程停止運行時,CLR強制終止仍在運行的任何后台線程,並且不拋出異常。

static void Main(string[] args)
{
Thread 某線程
= new Thread(線程回調函數);
某線程.IsBackground
= true;
某線程.Start(
"hello");
Console.WriteLine(
"某線程運行開始");
Console.WriteLine(
"繼續運行");
}
private static void 線程回調函數(Object 狀態參數) {
Thread.Sleep(
10000);
if (狀態參數.GetType() == typeof(string))
{
Console.WriteLine(
"這是一個字符串");
}
else {
Console.WriteLine(
"未識別");
}
}

如果某線程為前台線程,那么應用程序在10秒后才終止,如果是后台線程,那么應用程序立即終止。

原因是如果是前台線程,那么在運行完Main函數后,還需要這個前台線程結束才終止應用程序,而如果轉為后台線程,那么就會在所有前台線程終止后立馬終止。

通過Thread對象來顯示創建線程的都是前台線程,但是可以通過IsBackgroun屬性來切換,而線程池都是后台線程。

 


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com