C#筆記 – 線程及異步(一) 線程基礎

線程
  • 對CPU進行虛擬化,Windows為每個進程提供了該進程專用的線程(功能相當於一個CPU)

  • 每個線程都有以下要素

    • 線程內核對象

      • 一種由OS為線程所創建的數據結構,包含

        • 一組對線程進行描述的屬性

        • 線程上下文

          • 包含CPU寄存器集合的內存塊

    • 線程環境塊

      • 用戶模式(應用程序代碼能快速訪問的地址空間)中分配和初始化的內存塊(耗用1個內存頁)

      • 包含線程的異常處理鏈首

        • 線程進入每個try塊時在鏈首插入一個節點

        • 退出try塊時從鏈首刪除該節點

      • 包含線程的「線程本地存儲」數據

      • 包含由GDI和OpenGL圖形使用的一些數據結構

    • 用戶模式棧

      • 線程棧

      • 存儲傳給方法的局部變量和實參

      • 包含一個「指出當前方法返回時,線程應該從甚麼地方接著執行」的地址

      • 默認分配1MB內存

    • 內核模式棧

      • 應用程序代碼向OS中的內核模式函數傳遞實參時使用

      • 從用戶模式的代碼傳給內核的任何實參,都會被Windows從用戶模式棧複製到內核模式棧

    • DLL線程連接/分離通知

      • 任何時候在進程中創建線程時,都會調用進程中加載所有非托管DLL的DllMain方法,並向該方法傳遞DLL_THREAD_ATTACH標志

      • 線程終止時,DllMain方法會被調用,並傳遞DLL_THREAD_DETACH標志

上下文切換
  • Windows任何時刻只能將一個線程分配給一個CPU

    • 該線程能運行一個「時間片」(量/量程)

    • 時間片到期,Windows上下文切換到另一個線程。切換操作如下:

      • 將CPU寄存器的值保存到當前正在運行的線程的內核對象內部的一個上下文結構中

      • 從現有線程集合中選出一個線程供調度

      • 將所選上下文結構中的值加載到CPU的寄存器中

    • 上下文切換的執行頻率大概是30毫秒一次

    • 假如一個線程進入了死循環,Windows會定期「搶占」它,將新線程分配給CPU,讓新線程(如任務管理器)有機會被執行/終止死循環線程

  • 性能消耗

    • 上下文切換,意味CPU現在是要執行一個不同的線程,而之前的線程的代碼和數據還在CPU的快存(cache)中

    • 新線程要執行的代碼和數據一般與現在快存中的不一樣,所有CPU必須訪問RAM來填充它的快存

    • 而30ms後,一次新的上下文切換又再次發生

    • 執行上下文切換的所需時間取決於CPU架構和速度;填充CPU快存所需時間取決於運行中的應用、快存大小等等,無法給出估算值

    • 盡可能避免上下文切換的發生

  • GC與上下文切換

    • GC時,CLR必須掛起所有線程,所以減少線程的數量也有效提高GC性能

線程調度與優先級
  • 每個線程的內核對象都包含一個上下文結構

    • 上下文結構反映了線程上一次執行完畢後CPU寄存器的狀態

    • 在一個時間片之後,Windows檢查現存所有的線程內核對象,只有那些沒有正在等待甚麼的線程才適合調度

      • Windows選擇一個可調度的線程內核對象,並上下文切換至該線程

  • Windows被稱為「搶占式多線程」

    • 因為線程可在任何時間停止(被搶占)並調度另一個線程

    • 我們無法保證自己的線程一直執行,也阻止不了其他線程的運行

優先級
  • 每個線程被分配了從0(最低)到31(最高)的優先級

    • 系統決定為CPU分配哪個線程時,首先檢查優先級31的線程,以一種輪流的方式調度它們

      • 如果優先級31的一個線程可以調度,就把它分配給CPU;該線程時間片結束後,系統再檢查有沒有另一個優先級31的線程可以運行

      • 也就是說,只要存在可調度的優先級為31的線程,系統就永遠不會將優先級0 ~ 30的任何線程分配給CPU

        • 「饑餓」(Starvation):較高優先級的線程占用了大部分的CPU時間,導致較低優先級的線程無法運行

      • 較高優先級的線程總是搶占較低優先級的線程

        • 如果有一個優先級為5的線程正在運行,而系統確定有一個較高優先級的線程準備好,系統會立即掛起低優先級的線程,將CPU分配給高優先級的線程

  • 系統啟動時會創建一個「零頁線程」

    • 該線程優先級為0,在沒有其他線程需要工作時,該線程會將系統RAM的所有空閒頁清零

優先級類
  • 為了方便我們分配線程的優先級,Windows公開了優先級系統的一個抽象層

    • 進程優先級類

      • 支持6個進程優先級

        • Idle

        • Below

        • Normal

        • Above Normal

        • High

        • Realtime

      • Realtime優先級相當高,甚至可能干擾操作系統任務,盡量避免使用

    • 相對線程優先級

      • 相對於進程優先級

      • 支持7個相對線程優先級

        • Idle

        • Lowest

        • Below Normal

        • Normal

        • Above Normal

        • Highest

        • Time-Critical

    • 進程是一個優先級類成員

      • 在進程中,要為各個線程分配相對優先級

  • 優先級類與優先級

    • 每個線程的優先級取決於兩個標準

      • 進程優先級類

      • 進程優先級類裡分配的線程優先級

    • 優先級類和優先級合併構成一個「基礎優先級」

    • 「動態優先級」

      • 每個線程都有一個

      • 線程調度器根據這個優先級決定要執行哪個線程

        • 最初時,「動態優先級」 = 「基礎優先級」

          • 系統可提升/降低動態優先級,避免線程在處理器時間內「饑餓」

        • 對於基礎優先級16 ~ 31之間的線程,系統不會提升它們的優先級;0 ~ 15之間的才會被動態提升

  • 進程優先級類和相對線程優先級的「優先級」映射

    •  IdleBelow NormalNormalAbove NormalHighRealtime
      Time-Critical151515151531
      Highest6810121526
      Above Normal579111425
      Normal468101324
      Below Normal35791223
      Lowest24681122
      Idle1111116
  • 實際上,Windows永遠不會調度進程,只調度線程

    • 進程會根據啟動它的進程來分配優先級

      • Windows資源管理器(在Normal優先類中生成)

  • 托管應用程序不應該表現為擁有它們自己的進程;它們表現在一個AppDomain中運行

    • 所以,托管應用程序不應該更改它們的進程的優先級類

    • 但是可以更改線程的「相對線程優先級」

      • Thread.Priority

前台線程和後台線程
  • CLR把每個線程要麼視為前台線程,要麼視為後台線程

  • 一個進程的所有前台線程終止時,CLR強制終止所有正在運行的後台線程

    • 前台線程:確實想完成的任務

    • 後台線程:非關鍵性任務

  • 在線程生存期中,任何時候都可以進行前後台的切換

    • 通過Thread對象顯式創建的線程默認都是前台線程

    • 線程池線程默認為後台線程

    • 進行托管執行環境的本機代碼創建的線程被標記為後台線程

參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter