文章目錄
多線程和線程池的c++實現
1. linux pthread庫中對線程的操作
1.1 線程的創建和資源回收
每一次調用pthread_create()都會創建一個子線程,如果子線程是joinable,則
必須顯式調用pthread_detach()將其變爲non-joinable自行釋放資源
or 顯式調用pthread_join()由主線程爲其釋放資源,否則會造成內存泄露.
默認創建的線程是joinable的.
這裏的資源到底是啥?
- 子線程從父線程拷貝的棧內存,使用pthread_join()由父線程清理或
pthread_detach()由系統清理,如pthread_create之前父線程中的局部變量 - 子線程自己申請的堆內存,使用清理函數pthread_cleanup_push()
和pthread_cleanup_pop(), 如線程內部malloc或new出的空間
參見https://www.cnblogs.com/cthon/p/9078042.html, TODO
線程執行的函數其中的參數,返回值,局部變量在線程執行完畢離開函數後均會自動
釋放,不在這裏所說的資源範圍內
如果主線程想要使用子線程的結果,則不能自顧自的直接返回,由2種可選方式:
- pthread_join()阻塞主線程直到子線程返回釋放子線程的資源
優點: 主線程阻塞,不佔用cpu資源
缺點: 有些業務情境下不希望主線程阻塞,主線程需要做其他的事情 - pthread_detach()由子線程自己結束後自行釋放資源,主線程使用while (true)
死循環持續運行
優點: 主線程並未阻塞,可以處理其他事情
缺點: 主線程持續佔用cpu資源
爲什麼pthread_create()傳入的函數爲void* func(void* arg)形式?
實際使用時不同場景傳入的參數類型不一致,數目不一致,因此統一使用void*類型,簡單的
單個參數直接轉換類型傳給形參,複雜的參數先構建結構體,然後傳入指向該結構體的指針
1.2 線程的互斥和同步
pthread_mutex_lock發生了什麼?
各個線程競爭上鎖,競爭成功則進入臨界區稱爲運行狀態,競爭失敗則進入阻塞狀態
競爭: cpu喚醒所有等待的線程,使他們變爲就緒,
競爭成功: 被cpu調度執行,進入運行狀態,開始執行臨界區代碼
競爭失敗: 阻塞,進入睡眠狀態,不佔用cpu資源,同時被放入等待資源的優先級隊列
pthread_cond_wait(&cond, &mutex)發生了什麼?
- 條件不滿足,阻塞,進入等待隊列並釋放鎖,一氣呵成,原子操作
- 條件滿足(允許生產,stock<10),從阻塞狀態喚醒進入就緒狀態,競爭上鎖,分步進行,
非原子操作
a) 上鎖成功,判斷stock<10,退出while循環,執行生產動作
b) 上鎖失敗,再次判斷stock,如果stock<10,則阻塞在mutex上;如果stock>=10,則執行
條件不滿足的動作,阻塞在條件變量上
雖然都是阻塞,但是阻塞的位置是不同的,阻塞在條件變量上是等待條件滿足,爲了同步
阻塞在mutex上是等待其他線程釋放鎖,爲了互斥訪問
pthread_cond_signal(&cond)發生了什麼?
我操作完了,可能會使得消費者等待的條件滿足,因此發出信號(條件不一定滿足,
滿足不滿足需要另一方的消費者自己寫代碼判斷,因爲條件是另一方定的)
2. 生產者-消費者的多線程模型
生產者-消費者模型,二者共享的資源是有限容量的倉庫,倉庫空時暫停消費,倉庫滿時
暫停生產,對生產和消費二者的同步通過這兩個條件變量實現;
生產-生產,消費-消費,生產-消費都需要互斥訪問,因爲無論生產還是消費都是寫操作,
使用互斥鎖來實現對倉庫容量的互斥訪問
3. 線程池
3.1 爲什麼需要線程池?
爲了充分利用cpu的計算資源(不讓cpu閒下來),使用多線程執行任務;
但cpu的核心數有限,不可能來一個任務創建一個線程來執行,因爲這樣的代價是:
a) 創建的線程數高於cpu的核心數,如果任務的執行時間較長,由於系統的調度機制,一個
線程還沒執行完,另一個線程開始被調度執行,cpu頻繁在線程間切換,浪費時間
b) 來一個任務創建一個線程,創建線程也需要時間
c) 任務不是一直在高峯狀態,任務低谷時間段大量的線程閒置,消耗內存資源
線程池是執行效率和內存開銷之間權衡的產物,預先創建一定數量的線程,放進一個池子裏,
當來了任務之後,放進等待隊列中,如果隊列不爲空,則向線程池發信號.其中的線程競爭
執行任務,執行完畢後將任務從等待隊列中移除
雖然線程池預先分配了系統內存資源,但
- 避免了持續創建新線程的開銷
- 雖然無法避免cpu在線程間的切換,但這種切換是可控的,因爲線程的數目是我們自己定的
3.2 線程池需要解決什麼技術問題?
1) 如何定義一個任務?
任務可理解爲 函數+輸入數據,一個任務提供了他需要處理的數據和處理這些數據的方法
就像去修車鋪修自行車,我需要向修車師傅(線程)提供我的漏氣自行車(數據)和我想對
這個漏氣自行車執行的操作(修理漏氣自行車),如果需要返回數據,使用輸出參數
因此可定義一個類,數據成員包括
a) 一個函數
b) 要處理的數據
成員方法:
Run(): 調用函數成員(傳入要處理的數據)
爲了方便擴展任務的類型,可定義抽象基類Task,定義Run()接口,子類自行提供任務要
調用的函數和處理的數據
2) 如何調度任務的執行?
把任務放進優先級隊列中,線程池根據優先級取出任務執行
3) 任務隊列如何通知線程池中的線程?
啊!這不剛好使用條件變量嗎
3.3 線程池類應當如何設計?
- 數據成員
- 線程池大小
- 存放線程的vector
- 存放任務隊列的deque(目前只採用簡單的先入先出策略,未使用優先級隊列)
- 靜態成員: 互斥鎖(執行任務之間的線程互斥,添加任務-執行任務的互斥)
- 靜態成員: 條件變量(任務隊列是否爲空)
- 接口
- 構造函數調用Create()創建給定數目的線程
- AddTask() 添加任務併發出信號
- StopAll() 調用pthread_join回收資源
- 方法
- Create()創建的線程存進vector,創建線程的pthread_create調用
靜態 void* Run(void* arg)方法 - Run()阻塞等待任務隊列非空,執行任務,並將任務從任務隊列中刪除
爲什麼mutex, cond, task_queue都要使用static修飾?
- 需要將其放在類內,不能使用全局變量
- 類內static修飾的變量與類外的全局變量等效
原因: 多個線程之間共享數據:
在一個文件的範圍內,多個線程執行同樣的函數,該函數訪問數據,該數據對不同函數
是共享的,因此從編程的邏輯來看數據的共享不是執行同一個函數的多個線程對函數內
定義的變量的共享,而是多個函數的共享,實際上每個線程都有自己的棧空間,棧空間
存放的是函數內定義的局部變量的拷貝
命名空間與文件對應,類與函數對應,類中的數據成員相當於函數內的局部變量,多個
線程執行同一個類的成員函數(當然必須爲static函數),共享的數據應當是不同類共享
的數據,因此需要聲明爲static,類內成員函數對數據成員的共享是類相比於函數額外
增加的優勢