目錄
- 設計思路
- 主線程運行邏輯
- task以及taskpool設計
- 詳細流程講解
- 完整代碼
- 打印結果
- 往期回顧
設計思路
線程池實際上就是一組線程,當我們需要異步執行一些任務時,經常要通過OS頻繁創建和銷毀線程,不如直接創建一組在程序生命周期內不會退出的線程。
當有任務需要被執行時,線程可以自動地去拿任務并執行,在沒有任務時,線程處于阻塞或者睡眠狀態。
我們選擇隊列存放任務,main函數中的thread作為任務的生產者,從隊列尾部插入任務。線程池中的線程作為消費者,從隊列頭部獲取任務。
復雜考慮的話,當線程池的線程在處理任務的過程中也產生了關聯任務,那么這個線程也是消費者。
隊列也可以設計得更加具有實用性,例如可以根據任務的優先級設計多個隊列,然后線程根據優先級獲取線程(也就是先查詢高優先級的隊列,為空再去查詢低優先級的隊列)。
綜上,我們的設計中會有多個線程訪問任務隊列,所以我們要解決線程池創建、向隊列投放任務、從隊列中獲取任務的線程互斥性。
同時線程池的清理、退出線程池中的工作線程、清理任務隊列,也是需要考慮的。
并且,為了能夠詳細獲知多線程獲取多任務的流程,我們需要對taskID和threadID進行輸出打印,std::cout
并不是線程安全的,所以我們也要實現互斥地cout。
這里我們統一使用互斥量std::mutex
+lock來實現互斥性
主線程運行邏輯
task以及taskpool設計
Task:
TaskPool:
內部變量:
詳細流程講解
1、創建線程池對象時,調用構造函數TaskPool(),并初始化布爾類型的標志m_bRunning為false,表示此時線程池對象中的線程不工作
2、調用線程池對象的初始化函數init()
,這里默認的線程數為5。
- m_bRunning置為true,表示線程池對象中的線程應該開始運行了
- 然后通過for循環,每次構造一個新的線程對象,并且綁定一個線程函數
TaskPool::threadFunc
,創建之后線程就開始工作了。然后打印出當前的線程id,注意此時需要上鎖,保證cout輸出正常。然后將線程對象送入m_threads
數組
3、接下來看看構造出來的每個線程在干啥:
while(true)
{{上鎖,訪問任務隊列// 注意由于隊列本身是個多線程共享資源,所以對于隊列取元素以及狀態判斷都是要先加鎖再操作隊列為空則一直循環 // 為什么要用循環判斷呢?// 這是因為wait()從阻塞到返回,不一定就是由于notify_one()函數造成的,還有可能由于系統的不確定原因喚醒(可能和條件變量的實現機制有關),這個的時機和頻率都是不確定的,被稱作偽喚醒,如果在錯誤的時候被喚醒了,執行后面的語句就會錯誤,所以需要再次判斷隊列是否為空,如果還是為空,就繼續wait()阻塞。{如果m_bRunning為false,說明此時應該中止線程操作,所以需要連著跳出兩個循環,所以直接用goto label吧否則就一直等待在這兒,wait()可以讓線程陷入休眠狀態,在消費者生產者模型中,如果生產者發現隊列中沒有東西,就可以讓自己休眠.}此時 獲取隊頭元素,并將隊頭元素出隊}// 為了減少鎖的粒度,接下來的操作不需要加鎖了,因為已經拿到了隊列中的元素執行Task對象的doIt()方法,也就是打印任務id和線程id
}
label :
打印當前線程id
4、for循環,創建10個Task對象,然后調用addTask方法,將task送入線程池中的任務隊列。顯然push操作是互斥的,所以需要先上鎖。然后打印任務id和線程id,最后通過條件變量的notify_one方法,通知一個掛起的線程去消費隊列里面的任務。
5、等待一段時間
6、調用線程池對象的stop方法,先設置m_bRunning標志為false,然后通過條件變量的notify_all方法,通知掛起的或者正在運行的所有線程,結束線程函數運行。然后等待所有線程join之后,退出。
7、跳出主線程,開始調用TaskPool對象的析構函數,也就是執行removeAllTasks方法,也就是將任務隊列里面的存的task指針進行reset,也就是減引用計數,shared_ptr指針如果引用計數減為0,會自動調用析構函數。為了線程安全,我們同樣需要對這塊代碼進行加鎖。
完整代碼
c++實現簡單線程池代碼
打印結果
Init a thread, id: 2
Init a thread, id: 3
Init a thread, id: 4
Init a thread, id: 5
Init a thread, id: 6
add a Task, id: 0, thread id is: 1
add a Task, id: 1, thread id is: 1
add a Task, id: 2, thread id is: 1
handle a task ,TaskID is: 1, thradID is:3
a task destructed , TaskID is: 1, thradID is:3
handle a task ,TaskID is: 2, thradID is:4
a task destructed , TaskID is: 2, thradID is:4
handle a task ,TaskID is: 0, thradID is:2
a task destructed , TaskID is: 0, thradID is:2
add a Task, id: 3, thread id is: 1
handle a task ,TaskID is: 3, thradID is:3
a task destructed , TaskID is: 3, thradID is:3
handle a task ,TaskID is: 4, thradID is:5
add a Task, id: 4, thread id is: 1
a task destructed , TaskID is: 4, thradID is:1
add a Task, id: 5, thread id is: 1
handle a task ,TaskID is: 5, thradID is:4
a task destructed , TaskID is: 5, thradID is:4
add a Task, id: 6, thread id is: 1
handle a task ,TaskID is: 6, thradID is:4
a task destructed , TaskID is: 6, thradID is:4
handle a task ,TaskID is: 7, thradID is:2
add a Task, id: 7, thread id is: 1
a task destructed , TaskID is: 7, thradID is:1
add a Task, id: 8, thread id is: 1
add a Task, id: 9, thread id is: 1
handle a task ,TaskID is: 9, thradID is:6
a task destructed , TaskID is: 9, thradID is:6
handle a task ,TaskID is: 8, thradID is:5
a task destructed , TaskID is: 8, thradID is:5
exit thread , threadID:3
exit thread , threadID:4
exit thread , threadID:2
exit thread , threadID:5
exit thread , threadID:6Process finished with exit code 0
往期回顧
C++多線程快速入門(四)shared_mutex以及讀寫鎖應用
C++多線程快速入門(三):生產者消費者模型與條件變量使用
C++多線程快速入門(二)共享數據同步以及數據競爭
C++多線程快速入門(一):基本&常用操作