程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> [CLR via C#]25. 線程基礎

[CLR via C#]25. 線程基礎

編輯:C#入門知識

  Microsoft設計OS內核時,他們決定在一個進程(process)中運行應用程序的每個實例。進程不過是應用程序的一個實例要使用的資源的一個集合。每個進程都賦予了一個虛擬地址空間,確保一個進程使用的代碼和數據無法由另一個進行訪問。這樣就確保了應用程序集的健壯性,因為一個進程無法破壞另一個進程裡的數據和代碼。另外,進程是無法訪問到OS的內核代碼和數據。   如果一個應用程序進入死循環時,如果只是單核的CPU的,它會無限循環執行下去,不能執行其他代碼,這樣會使系統停止響應。對此,Microsoft拿出的一個解決方案——線程。線程的職責就是對CPU的虛擬化。Windows為每個進程都提供了該進程專用的線程(功能相當於一個CPU,可將線程理解成一個邏輯CPU)。如果應用程序的代碼進入無限循環,與那個代碼關聯的進程會"凍結",但其他進程不會凍結,會繼續執行。         線程盡管非常強悍,但和一切虛擬化機制一樣,線程會產生空間(內存耗用)和時間(運行時的執行性能)上的開銷。
  • 線程內核對象(thread kernel object)    OS為系統中創建的每個線程都分配並初始化這種數據結構。在該數據結構中,包含一組對線程進行描述的屬性。 數據結構中還包含所謂的線程上下文(thead context)。上下文是一個內存塊,其中包含了CPU的寄存器集合。Windows在一台x86CPU的計算機運行時,線程上下文使用約700字節的內存。對於x64和IA64CPU,上下文分別使用約1240字節和2500字節的內存。       
  • 線程環境塊(thread environment block,TEB)    TEB是在用戶模式中分配和初始化的一個內存塊。TEB耗用1個內存頁(x86和x64CPU中是4KB,IA64CPU中是8K)。TEB包含線程的異常處理鏈首。線程進入的每個try塊都在鏈首插入一個節點。線程退出try塊時,會從鏈中刪除該節點。除此之外,TEB還包括線程的"線程本地存儲"數據,以及由GDI和OpenGL圖形使用的一些數據結構。
  • 用戶模式棧(user-mode stack)    用戶模式棧用於存儲傳給方法的局部變量和實參。它還包含一個地址:指向當前方法返回時,應該接著從哪個地址開始執行。默認情況下,Windows為每個線程的用戶模式分配1MB的內存。
  • 內核模式棧(kernel-model stack)    應用程序代碼向OS中的一個內核模式的函數傳遞實參時,會使用內核模式棧。出於安全方面的原因,針對從用戶模式的代碼傳給內核的任何實參,Windows都會把它們從線程的用戶模式棧復制到線程的內核模式棧。一經復制,內核就可以驗證實參的值,然後進行處理。除此之外,內核會調用它自己內部的方法,並利用內核模式棧傳遞自己的實參、存儲函數的局部變量以及存儲返回地址。在32為的Windows運行時,內核模式棧大小為12KB;在64位Windows上運行時,大小為24KB。
  • DLL線程連接(attach)和線程分離(detach)通知 Windows的一個策略是,任何時候在進程中創建一個線程,都會調用那個進程中加載的所有DLL的DLLMain方法,並向該方法傳遞一個DLL_THREAD_ATTACH標識。類似的,任何時候一個線程終止,都會調用進程中的所有DLL的DLLMain方法,並向該方法傳遞一個DLL_THREAD_DETACH標識。有的DLL需要利用這些通知,為進程中創建和銷毀的每個線程執行一些特殊的初始化或資源清理操作。
  現在,你已經知道了創建線程、讓它進駐系統以及最後銷毀它所需要的全部空間和時間的開銷。現在我們開始討論上下文切換。   單CPU的計算機一次只能做一件事。所以,Windows必須在系統中的所有線程之間共享物理CPU。   在任意時刻,Windows只將一個線程分配給一個CPU。那個線程允許運行一個"時間片"。一旦時間片到期,Windows將上下文切換到另一個線程,每次上下文切換都要求Windows執行以下操作。
  • 將CPU寄存器中的值保存到當前正在運行的線程的內核對象內部的一個上下文結構中。
  • 從現有線程集合中選出一個線程供調度(這個就是要切換到的線程)。如果該線程由另一個進程擁有,Windows在開始執行任何代碼或者任何數據之前,還必須切換CPU"看見"的虛擬地址空間。
  • 將所選上下文結構中的值加載到CPU的寄存器中。
  上下文切換完成後,CPU執行所選的線程,直到它的時間片到期。然後,會發生另一次上下文切換。Windows大約每30毫秒執行一次上下文切換。上下文切換是淨開銷;也就是說,上下文切換所產生的開銷不會換來任何內存或性能上的收益。Windows執行上下文切換,向用戶提供一個健壯的、響應靈敏的操作系統。   事實上,上下文切換對性能的影響可能超出你的想象。CPU現在是要執行一個不同的線程,而之前的線程代碼和數據還保存在CPU的高速緩存中,這使CPU不必進程訪問RAM。當Windows上下文切換到一個新的線程時,這個新線程極有可能要執行不同的代碼和數據,這些數據不再CPU的高速緩存中,因此,CPU必須訪問RAM來填充它的高速緩存,以恢復告訴執行狀態。但是,在30毫秒之後,一次新的上下文切換又發生了。   除此之外,執行垃圾回收時,CLR必須掛起所有線程,遍歷它們的棧來查找根以便對堆中的對象進行標記,再次遍歷它們的棧,再次恢復所有線程。所以,減少線程的數量也會顯著提升垃圾回收器的功能。   根據上述討論,我們的結論是必須盡可能地避免使用線程,因為它們要耗用大量內存,而且需要相當多的時間來創建、銷毀和關聯。WIndows在進行上下文切換,以及垃圾回收時也會浪費更多的時間。但是不可否認,因為才是Windows變得更健壯,反應更靈敏。   應該指出,安裝多個CPU的計算機可以真正同時允許幾個線程,這提升應用程序的可伸縮性(在更少的時間內做更多的事)。Windows為每個CPU內核都分配一個線程,每個內核都自己執行到其他線程的上下文切換。Windows確保單個線程不會同時在多個內核上調度。       如果追求性能,那麼任何計算機最優的線程數就是那台計算機的CPU個數。如果線程數超過了CPU的個數那麼就會發生線程上下文切換和性能損失。   在Windows中,創建一個進程的代價是昂貴的。創建一個進程通常要花幾秒鐘的時間,必須分配大量的內存,這些內存必須初始化,EXE和DLL文件必須從磁盤上加載等等。相反,在Windows創建線程是十分廉價的。所以,開發人員決定停止創建進程,改為創建線程。這就是我們看到有這麼多線程的原因。但是,線程相對於其它系統資源還是比較昂貴的,所以還是應該省著用。   必須承認,系統中的大多數線程都是本地代碼創建的。所以,線程的用戶模式棧僅僅保留(預定)地址空間,而且極有可能沒有完全提交來獲取物理內存。然而,隨著越來越多的應用程序成為托管應用程序,或者在其中運行托管組件,會有越來越多的棧被完全提交,會真實的分配到1MB的物理內存。無論如何,即使拋開用戶模式棧不談,所有線程仍然會分配到內核模式棧以及其它資源。這種覺得線程十分廉價便胡亂創建線程的勢頭必須停止。       CLR現在用的是Windows的線程處理能力。   雖然現在CLR線程直接對應一個Windows線程,但Microsoft CLR團隊保留了將來把它從Windows線程中分離的權限。有一天,CLR可能引入它自己的邏輯線程概念,使一個CLR邏輯線程並非一定映射到一個物理Windows線程。據說,邏輯線程將使用比物理線程少的多的資源,所以能在極少量的物理線程上運行大量的邏輯線程。            本節將展示如何創建一個線程,並讓它執行一次異步計算限制操作。雖然會教你具體如何做,但是強烈建議你避免采用這裡展示的技術。相反,應該盡量使用CLR的線程池來執行異步計算限制操作,具體以後會討論。   如果執行的代碼要求處於一種特定的狀態,而這種狀態對於線程池的線程來說是非比尋常的,就可以考慮創建一個線程 。例如,滿足以下任意一個條件,就可以顯式創建自己的線程。
  • 線程需要以非普通線程優先級運行。所有線程池線程都以普通優先級運行;雖然可以改變這種優先級,但不建議這樣做。另外,在不同的線程池操作之間,對優先級的更改是無法持續的。
  • 需要線程表現為一個前台線程,防止應用程序在線程結束它的任務之前終止。線程池的線程都是後台線程。如果CLR想要終止線程,它們可能被迫無法完成任務。
  • 一個計算限制的任務需要長時間運行。線程池為了判斷是否需要創建一個額外的線程,所采用的邏輯是比較復雜的。直接為長時間運行的任務創建一個專用線程,則可以避免這個問題。
  • 要啟動一個線程,並可能調用Thread的Abort方法來提前終止它。
    為了創建一個專用線程,要構造System.Thearding.Thread類的一個實例,向它的構造器傳遞一個方法的的名稱。以下是Thread構造器的原型:
public sealed class Thread : CriticalFinalizerobject,... {
    public Thread(ParameterizedThreadStart start);
    //這裡沒有列出不常用的構造器
}

delegate void ParameterizedThreadStart(Oject obj);
  構造Thread對象是一個輕量級操作,因為它並不實際創建一個操作系統線程。要實際創建操作系統線程,並讓它開始執行回調方法,必須調用Thread的Start方法,向它傳遞要作為回調方法的實參傳遞的對象(狀態)。以下代碼演示了如何創建一個專用線程,並讓它異步調用一個方法:
internal static class FirstThread {
   public static void Go() {
      Console.WriteLine("Main thread: starting a dedicated thread " +
         "to do an asynchronous operation");
      Thread dedicatedThread = new Thread(ComputeBoundOp);
      dedicatedThread.Start(5);
 
      Console.WriteLine("Main thread: Doing other work here...");
      Thread.Sleep(10000);     // 模擬做其它工作(10 秒鐘)
 
      dedicatedThread.Join();  // 等待線程終止
      Console.ReadLine();
   }
 
   // 這個方法的前面必須和ParametizedThreadStart委托匹配
   private static void ComputeBoundOp(Object state) {
      // 這個方法由一個專用線程執行
      Console.WriteLine("In ComputeBoundOp: state={0}", state);
      Thread.Sleep(1000);  // 模擬其它任務(1 秒鐘)
 
      // 這個方法返回後,專用線程將終止
   }
}
  在我的機器上編譯運行,可能得到以下結果: Main thread: starting a dedicated thread to do an asynchronous operation Main thread: Doing other work here... In ComputeBoundOp: state=5   但有的時候運行上述代碼,也可能得到以下結果,因為我無法控制Windows對兩個線程進行調度的方式: Main thread: starting a dedicated thread to do an asynchronous operation In ComputeBoundOp: state=5 Main thread: Doing other work here...   注意Go()方法調用的Join。Join方法造成調用線程阻塞當前執行的任何代碼,直到dedicatedThread所代表的那個線程銷毀或終止。         使用線程有以下三方面的理由:
  • 可以使用線程將代碼同其他代碼隔離    這將提高應用程序的可靠性。事實上,這正是Windows在操作系統中引入線程概念的原因。
  • 可以使線程來簡化編碼    有的時候,如果通過一個任務自己的線程來執行該任務,編碼會變得更簡單。通常,在你引入線程時,引入的是要相互協作的代碼,它們可能要求線程同步構造知道另一個線程在什麼時候終止。一旦開始涉及協作,就要使用更多的資源,同時會使代碼變得更復雜。所以,在開發使用線程之前,務必確定線程真的能幫到你。
  • 可以用線程來實現並發處理    如果知道自己的應用程序要在多CPU機器上運行,那麼讓多個任務同時運行,就能提高性能。
    搶占式(preemptive)操作系統必須使用某種算法判斷在什麼時候調度哪些線程多長時間。本節討論Windows采用的算法。在前面,已經提到過每個線程的內核對象都包含一個上下文結構。上下文結構反映了當線程上一次執行時,線程的CPU寄存器的狀態。在一個時間片之後,Windows檢查現有的所有線程內存對象。在這些對象中,只有那些沒有正在等待什麼的線程才適合調度。      Windows選擇一個可調度的線程內核對象,並上下文切換到它。Windows實際記錄了每個線程被上下文切換到的次數。可以使用向Microsoft Spy++這樣的工具查看這個數據。   Windows之所以被稱為一種搶占式多線程操作系統,是因為線程可以在任何時間被停止(被搶占),並調度另一個線程。所以,你不能保證自己的線程一直在運行,不能阻止其他線程的運行。   每個線程都分配了從0(最低)—31(最高)的一個優先級。系統決定將哪個線程分配給一個CPU時,它首先檢查優先級31的線程,並以一種輪流的方式調度它們。   只要存在可以調度的優先級31的線程,系統永遠不會將優先級0-30的任何線程分配給CPU。這種情況稱為饑餓(starvation)。當較高優先級的線程占用了太多的CPU時間,致使較低優先級的線程無法運行時,就會發生這種情況。在多處理器機器上饑餓發生的可能性要小得多,因為這種機器上優先級31的線程和優先級30的線程可以同時運行。系統總是保持各CPU處於忙碌狀態,只有沒有線程可調度的時候,CPU才空閒下來。   較高優先級的線程總是搶占較低優先級的線程,無論正在運行的是什麼較低優先級的線程。   系統啟動時,會創建一個名為零頁線程的特殊線程。這個線程的優先級定位0,而且整個系統中唯一一個優先級為0的線程。零頁線程負責在麼有其它線程需要執行的時候,將系統的RAM的所有空閒頁清零。   設計應用程序時,應決定自己的應用程序是需要比機器上同時運行的其它應用程序更高還是更低的響應能力。然後,選擇一個進程優先級類(priority class)來反映你的決定。Windows支持6個進程優先級類:Idle(空閒),Below Noral(低於標准),Normal(標准),Above Normal(高於標准),High(高)和Realtime(實時)。由於Normal是默認優先級類,所以它是最常用的優先級類。   優先級類和優先級是兩個不同的概念。根據定義,線程的優先級取決於兩個標准:1)它的進程優先級類 2)在其進程的優先級類中,線程的優先級。進程優先級類和線程優先級構成了一個線程的"基礎優先級"。注意,每個線程都有一個動態優先級。線程調度器是根據優先級來決定運行哪個線程。最初,線程的動態優先級適合基礎優先級一樣的,系統可提升或降低動態優先級,以確保它的響應,並避免現在在處理器時間內"饑餓"。但是,基礎優先級在16-31之間的優先級線程,系統不會提升它們的優先級,在0-15優先級之間的線程才會被動提升優先級。   如果一個應用程序(比如屏幕保護程序),在系統什麼事情都不做的時候運行,就適合分配Idle優先級類。一些執行統計學跟蹤分析的應用程序需要定期更新於體統有關的狀態,這種應用程序一般不應該妨礙執行更關鍵的任務。   只有在絕對必要的時候才應使用High優先級類。Realtime優先級類要經可能的避免。Realtime優先級相當高,它甚至可能干擾操作系統任務,比如阻礙一些必要的磁盤I/O和網絡傳輸。   選好一個優先級類後,就不要再思考你的應用程序和其他應用程序的關系了。現在,應該將所有注意力放在應用程序中的線程上。Windows支持7個相對線程優先級Idle(空閒),Lowest(最低),Below Normal(低於標准),Normal(標准),Above Normal(高於標准),Highest(最高)和Time-Critical(關鍵時間(最高的相對線程優先級))。這些優先級相對於進程優先級類的。同樣的,由於Normal是默認的相對線程優先級,所以最常用。     這裡並沒有提到有關0~31的優先級的任何內容。開發者從來不用具體設置一個線程的優先級,也就是不需要將一個線程優先級設置為0~31中的一個。操作系統負責將“優先級類”和“相對線程優先級”映射到一個具體的優先級上。這種映射方式,是隨Windows版本的不同而不同的。

線程相對

優先級

進程優先級類

Idle

Below Normal

Normal

Above Normal

High

Real-Time

Time-critical

15

15

15

15

15

31

Highest

6

8

10

12

15

26

Above normal

5

7

9

11

14

25

Normal

4

6

8

10

13

24

Below normal

3

5

7

9

12

23

Lowest

2

4

6

8

11

22

Idle

1

1

1

1

1

16

       請注意,表中線程優先級沒有為0的。這是因為0優先級保留給零頁線程了,系統不允許其他線程的優先級為0。而且,以下優先級也是不可獲得的:17,18,19,20,21,27,28,29和30。當然,如果編寫的是運行在內核模式的設備卻、驅動程序,可以獲得這些優先級。   注意:"進程優先級類"的概念容易引起一些混淆。人們可能認為這意味著Windows能調度進程。然而,Windows永遠不會調度進程;它調度的只有線程。"進程優先級類"是Microsoft提出的一個抽象概念,旨在幫助你理解自己的應用程序和其它正在運行應用程序的關系,它沒有其它用途。   提示:最好是降低一個線程的優先級,而不是提升另一個線程的優先級。   在你的應用程序中可以更改它的線程的相對線程優先級,這需要設置Thread的Priority屬性,向它傳遞ThreadPriority枚舉類型中定義的5個值之一,即Lowest(最低),Below Normal(低於標准),Normal(標准),Above Normal(高於標准),Highest(最高)。CLR為自己保留了Idle和Time-Critical優先級。    應該指出的是,System.Diagnostics命名空間包含一個Process類和一個ProcessThread類。這兩個類分別提供了進程和線程的Windows視圖。應用程序需要以特殊的安全權限運行才能使用這兩個類。例如,在Silverlight應用程序或者ASP.NET應用程序中,就不可以使用這兩個類。   另一方面,應用程序可使用AppDomain和Thread類,它們公開了AppDomain和線程的CLR視圖。一般不需要特殊安全權限來使用這兩個類,雖然某些操作仍需要提升權限才可以。            CLR將每個線程要麼視為前台線程,要麼視為後台線程。一個進程中的所有前台線程停止時,CLR會強制終止仍然在運行的任何後台進行。這些後台進程被直接終止,不會拋出異常。   因此,前台進程應該用於執行確實想完成的任務,比如將數據從內存緩存區fluch到磁盤。另外,應該為非關鍵的任務使用後台線程,比如重新計算電子表格的單元格,或者為記錄建立索引。這是由於這些工作能在應用程序重啟時繼續,而且如果用戶終止應用程序,就沒有必要強迫它保持活動狀態。   CLR要提供前台線程和後台線程的概念來更好地支持AppDomain。每個AppDomain都可以運行一個單獨的應用程序,每個應用程序都有它自己的前台線程。如果一個應用程序退出,造成它的前台線程終止,則CLR仍然需要保持活動並運行,使其他應用程序繼續運行。所有應用程序都退出,它們的所有前台線程都終止後,整個進程就可以被銷毀了。
public class Program
    {
        public static void Main()
        {
            // 創建一個線程 (默認是前台進程)
            Thread t = new Thread(Worker);
 
            // 將前台進程變成後台進程
            t.IsBackground = true;
 
            t.Start(); // 啟動線程
            // 如果t是一個前台進程,則應用程序大約10秒後才終止
            // 如果t是一個後台進程,則應用程序立即終止
            Console.WriteLine("Ruturning from Main");
 
            Console.Read();
        }
 
        private static void Worker()
        {
            Thread.Sleep(10000); // 模擬做10秒鐘工作
            Console.WriteLine("Ruturning from Worker");
        }
    }
  在一個線程的生存期,任何時候可以從前台變成後台,或者從後台變成前台。應用程序的主線程以及通過構造一個Thread對象來顯式創建的任何線程都默認為前台線程。另一方面,線程池默認為後台線程。此外,由進入托管執行環境的本地代碼創建的任何線程都被標記為後台線程。   提示:要盡量避免使用前台線程。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved