Bo2SS

Bo2SS

6 多執行緒程式設計基礎

課程內容#

進程間通信複雜 [內存空間獨立]、切換成本高 [時間局部性],所以發明了執行緒

執行緒#

一個進程的分支 [pthread],本質上是一個輕量級的進程

  • 通信方便,因為一個進程裡的多個執行緒共享內存
  • 切換成本低,因為內存是共享的,在切換執行緒時不需要置換快取

pthread_creat#

創建一個新的執行緒

  • man pthread_create
  • 原型
    • 圖片
    • thread:執行緒 id [注意:不是數字類型,不能直接用 == 判斷,見 pthread_equal]
    • attr:屬性
    • arg 是 start_routine 的參數
  • 描述
    • 圖片
    • 開啟一個新的執行緒,並執行 start_route 函數
      • start_route 函數只能收一個 arg 參數 [多個參數可用結構體封裝]
    • 執行緒有4 種終結的方式 [作為工具人,死亡方式很重要]
      • ① 自殺:自己調用 pthread_exit
        • 同一進程下的執行緒可使用 pthread_join 接收它的死亡狀態 [有點像 wait]
      • ② 正常死亡:從 start_routine 函數中返回
        • 與 pthread_exit 方式等價
      • ③ 他殺:pthread_cancel
      • ④ 同歸於盡:進程中的一個執行緒調用 exit,或者主執行緒從 main 函數中返回
        • [PS] 如果一個執行緒導致內存崩潰,極有可能也產生同歸於盡的效果,即進程中的所有執行緒都死亡
    • attr 可以為 NULL,對應默認屬性
    • 調用成功後,會將執行緒 id 保存在 thread 變量中,後續都可以通過這個 id 使用它 [類似文件描述符]
  • 返回值
    • 圖片
    • 0,成功;否則,失敗

——3 種終止執行緒的方法 ——#

pthread_exit#

執行緒自殺

  • man pthread_exit
  • 圖片
  • ❗ 執行緒自殺,將 retval 傳給 join 的執行緒 [執行緒默認是可加入的]
  • 執行完 pthread_cleanup_push 註冊的函數,隨後釋放執行緒特有的數據
    • 進程中共享的資源不會被釋放 [因為還有兄弟執行緒]
    • atexit 註冊的函數不會被調用 [這是屬於進程的]
  • 在最後一個執行緒結束後,進程以 exit (0) 的方式結束,釋放進程共享的資源,並執行 atexit 註冊的函數
  • 【注意】執行緒和進程的關係

pthread_cancel#

給一個執行緒發送取消請求 [他殺]

  • man pthread_cancel
  • 圖片
  • 執行緒取消的可能性和時間取決於兩個屬性:state、type
  • state
    • 可殺 [默認]
    • 不可殺:此時收到的取消命令會排在隊列中
  • type
    • 推遲的 [默認]:等到執行緒的下一次調用
    • 異步的:立即,但系統不能保證

exit [進程相關]#

終止普通進程

  • man exit
  • 圖片
  • 傳給父進程的值為:status & 0377
    • 注意:0377 是 8 進制,對應二進制的 8 個 1,也就是只保留 status 的低 8 位
  • 使用 atexit 和 on_exit 註冊的函數會以與註冊相反的順序被調用
    • 可以套娃:註冊的函數中還可以有註冊,並且會放在調用列表的最前面
    • 如果註冊的函數沒有返回,比如調用了_exit 或者使用信號自殺,剩余的函數不會再被調用,exit 相關的處理也會被禁止
    • 多次註冊函數,會被多次調用
  • ⭐ exit 後會刷新並關閉所有標準 IO 流

—— 監控執行緒狀態相關 ——#

pthread_join#

等待執行緒終止

  • man pthread_join
  • 圖片
  • 可類比進程中的 wait 函數
  • retval 接收執行緒退出狀態
    • 如果執行緒是自殺的,則複製 pthread_exit 中的 retval 值
    • 如果執行緒是他殺的,則賦值為 PTHREAD_CANCELED
  • [思考] 這裡的 retval 是二級指針,也就是指針的指針,為什麼?

pthread_detach#

分離執行緒

  • man pthread_detach
  • 圖片
  • 執行緒被分離後,其終止時系統會自動回收其資源,而不需要其他執行緒阻塞等待它終止了
  • 一般與pthread_self配合使自己分離
    • pthread_self:獲得調用執行緒 [自己] 的 id
  • 參考pthread_join () 與 pthread_detach () 詳解——CSDN

—— 附加 ——#

pthread_yield#

讓出處理器 [processor]

  • man pthread_yield
  • 圖片
  • [類似 sleep 的效果]
  • 該方法只用於某些系統,更標準的用法:sched_yield
    • 對於協同式系統,調用此函數來主動讓出 CPU
    • 對於搶佔式系統,內核會進行調度,該函數沒有太大意義,也可直接使用 sleep
    • 協同式與搶佔式,詳見 4 高級進程管理 ——調度器分類

pthread_equal#

對比兩個執行緒的 id [不能直接用 == 判等]

  • man pthread_equal
  • 圖片
  • 如果相等,返回一個非零值

執行緒池#

讓一堆執行緒在池子裡待命,隨時工作

基本組成部分👇

任務隊列:存放需要處理的任務

  • [循環隊列更佳]
  • 基本操作:init、push、pop

② 多個執行緒:時刻準備著,減少創建響應和銷毀的時間

③ 執行緒功能:do_work ()

  • while (1):等待任務加入
      1. 任務隊列 pop:彈出任務,供執行緒執行
      1. do-work ():在CPU中執行任務

❗ 注意:push 和 pop 的時候都需要加鎖,防止數據競爭 [饑渴的執行緒]

  • 詳見代碼演示

內核執行緒、用戶執行緒#

執行緒是誰產生的?執行緒模型

  • 兩者的區別主要在調度上:內核執行緒由內核調度;用戶執行緒由用戶進程調度
  • 內核執行緒的優勢
    • ① 每個內核執行緒有自己的時間片。所以其進程因為有多個執行緒,會擁有更多的處理器時間;而用戶進程不會因為多分出了幾個用戶執行緒獲得更多的處理器時間
    • ② 如果一個內核執行緒被阻塞,進程中剩余的執行緒還可以繼續運行。如果一個用戶執行緒被阻塞,整個進程就都會被阻塞
      • PS:如果一個內核執行緒給自己的進程發送 sleep 信號,這個執行緒依然可以繼續運行
  • 用戶執行緒的優勢
    • ① 切換成本低。不會涉及用戶態向內核態的轉換
    • ② 調度算法完全由進程控制。用戶進程可以用自己的調度算法,所以自主性更好;而內核執行緒的調度,對用戶來說是一個黑箱
  • 所以可以結合兩者的優勢,設計既有內核執行緒,也有用戶執行緒的混合式執行緒

代碼演示#

簡單使用多執行緒#

  • 圖片
  • 圖片
  • 關注:pthread_create 函數的使用
  • 如果在創建執行緒後,沒有 usleep 或者 usleep 的時間過短,可能發生:
    • 主執行緒 return,從而使所有子執行緒同歸於盡
    • 此時會發生某些輸出出現兩次的情況,如①、② [③為正常輸出]
    • 圖片
    • 猜測:輸出緩衝區的問題,在執行緒突然結束時,又輸出了一次緩衝區的內容 [緩衝區沒來得及更新]
    • [PS] 使用 fflush 也不能解決,可能因為執行緒是突然結束的
  • 注意:所有執行緒都可以操作同一個地址的值

執行緒池#

thread_pool.h

  • 圖片
  • 任務隊列的定義,及基本操作

thread_pool.c

  • 圖片
  • 圖片
  • 注意:加解鎖、發信號;判斷隊列滿 / 空;判斷指針是否到達隊尾

1.test.c

  • 圖片
  • 圖片
  • 存儲數據的 buff 為二維數組,可避免執行緒讀取數據時,主執行緒通過地址改變了數據
  • usleep 的妙用:避免 while (1) 循環太耗 CPU
  • pthread_detach 一般與 pthread_self 配合使用
  • fgets:讀取文件到緩衝區
    • man fgets
    • 圖片
    • 圖片
    • 按行讀
  • 輸出效果
    • 圖片
    • 基本輸出:push [任務隊列輸出];pop [任務隊列輸出] + do_work [執行緒輸出]

附加知識點#

  • 編譯包含執行緒相關函數的文件時,注意使用 - lpthread

Tips#


載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。