線程池的目的:
1.復用線程,減少頻繁創建和銷毀的開銷
創建和銷毀線程是昂貴的系統操作,涉及內核調度、內存分配;
使用線程池預先創建一批線程,在多個任務間循環復用,避免資源浪費,提高性能。
2.主線程不阻塞,讓異步線程處理任務
主線程可以專注于接收任務或調度邏輯;
實際的耗時操作(如 IO、計算)由線程池中的工作線程執行;
這樣可以提升程序響應速度、增強并發能力。
3.控制線程數量,避免線程過多導致資源競爭
線程池原理:
任務隊列+一組工作線程+條件變量調度機制(喚醒阻塞的工作線程處理任務)
1.任務隊列 1.提供push放入任務的接口 2.pop獲取任務的接口(沒有 工作線程會阻塞) ????????3.cancel析構線程池?通過mutex保證同步+condition_variable控制工作線程
2.調度器 1.初始化創建工作線程 2.提供工作線程的入口函數work()從隊列中循環pop獲取任務處理 沒有任務會阻塞的pop函數中。3.提供Post 主線程向隊列中放入任務的接口。
單生產者對多消費者(單隊列)
1.調度器ThreadPool
1.構造函數
創建一定數量的工作線程?并進行管理。
2.Woker() 工作線程的入口函數
循環處理隊列中的任務,通過Pop()獲取任務 輸出型參數task帶回任務,沒有任務就回阻塞在Pop()函數中。
3.Post() 向隊列中放入任務的接口
bind綁定函數 將閉包對象存入任務隊列中
4.析構函數
對每個退出的線程進行join()等待回收
2.任務隊列
1.Push()入隊列接口
先加鎖 把任務放入到隊列中 解鎖后再用條件變量喚醒一個工作線程,進行處理。
2.Pop() 出隊列
輸出型參數value 返回給上層任務。條件變量阻塞線程,等有隊列有任務才會解除阻塞 或者退出的時候。
3.Cancel()?
線程池析構邏輯,設置為非阻塞_nonblock=true,并喚醒所有線程 進行退出。
此時在Pop()被阻塞的線程被喚醒 返回false,工作線程break退出循環。
多生產者對多消費者(雙隊列)
上面單隊列的適合一個生產者對應多個消費者的場景,如果多對多 那鎖的沖突就大了。對此我們采用的是雙緩沖區的策略,即生產者和消費者各一個隊列。
工作線程消費完工作隊列中的數據后 會進行交換隊列的操作,繼續進行處理。
1.調度器
不改變也沒問題,但對于這個向隊列中放入任務的Post()函數 原本只能存入std::function<void()>類型的函數。為了解決函數有返回值,上層獲取到返回值,我們這樣設置Post函數。
這是一個線程池任務提交函數(Post),它接受任意可調用對象
f
和參數args...
,并返回一個std::future
,用于異步獲取結果。1.實現要解決 傳入的函數F的返回值類型是什么。可以用類型萃取工具std::invoke_result_t<>獲取F函數 參數Args...的返回值類型。但參數的類型等模板推導完才行,因此采用了
auto post()->類型 尾置返回值類型的語法(告訴編譯器等模板推導完再 告訴你返回值的類型)
2.現在知道了返回值類型,怎么把返回值給上層?
1.promise+futrue 在任務函數中手動設置結果。但還要更好的選擇
2.packaged_task<>+future異步任務封裝器,對函數進行封裝 返回閉包對象。讓線程執行這個閉包對象 等函數執行完自動把結果進行設置,就不用手動設置結果了。
1.template <typename F, typename... Args>????????
? ?F代表函數類型? ...Args函數的參數? ? ?
2.std::invoke_result_t<F, Args...>類型萃取工具
????用來獲取調用某個函數對象
F
以參數Args...
所產生的返回類型。讓編譯器自己推導函數的返回類型?3.auto Post(...) -> std::future<std::invoke_result_t<F, Args...>>尾置返回類型語法
?它是函數返回類型的一種寫法,主要用于模板函數中返回類型依賴參數類型的情況。
? ? ? ? 1.auto讓編譯器先跳過函數的返回類型 先去處理函數的參數
? ? ? ? 2.推導出函數參數F Args...的類型
? ? ? ? 3.推導完函數參數的類型 才能推導出函數的返回值類型
為什么不能寫成這樣?
std::future<std::invoke_result_t<F, Args...>> Post(F&& f, Args&&... args);
因為編譯器解析函數聲明時是從左到右的,獲取返回值的類型std::future<std::invoke_result_t<F, Args...>> 但F Args..的類型必須等參數傳入后,才能確認。
編譯器處理函數模板時是按“返回值 → 函數名 → 參數”的順序進行的,你不能在返回值里使用還沒推導出的模板參數類型,這就是為什么需要尾置返回類型的根本原因。
4.std::packaged_task<>異步任務封裝器
std::packaged_task 是一個 把函數和返回值綁定起來的包裝器,可以讓函數的執行結果通過 std::future 異步獲取。
它是 C++ 標準庫中為了解決“函數執行完以后怎么拿返回值”的問題而設計的組件。
eg.使用方法
int add(int a, int b) { return a + b; } //1.用 packaged_task+bind 包裝它函數 std::packaged_task<int()> task(std::bind(add, 2, 3)); //2.獲取關聯的future std::future<int> result = task.get_future(); //3.異步執行這個任務(線程、線程池、手動調用都可以) std::thread t(std::move(task)); // 線程里執行 task() t.join(); //4.fet()獲取返回值 std::cout << result.get();
和promise最大的區別是,不需要手動設置set_value的值,等bind綁定的函數執行完 自動把函數的返回值填入,之后get()就可以獲取到函數的返回值。
對比點 std::packaged_task
std::promise
是否自動設置 future 的值? ? 是的,執行函數后自動設置 ? 需要你手動調用 .set_value()
誰負責填充結果? 函數執行完后自動填充 你手動調用 .set_value()
是否必須綁定函數? ? 是的 ? 否,傳的是值 packaged_task 最適合線程池,因為線程池就是“執行任務然后獲得返回值”,它天然對接任務隊列。
“執行一個函數并得到它的返回值” —— 用 packaged_task;
promise 更多用于線程間通信,或者異步控制信號(比如網絡回調、I/O完成通知等)。
“告訴另一個線程一個值” —— 用 promise。
2.任務隊列(雙)
和單隊列不同的地方是,Pop()工作線程獲取任務時 如果沒有任務,不會阻塞在Pop()而是會進行Swap()交換隊列(生產者隊列有數據或者非阻塞)
? ? ? ? if(_con_queue.empty()&&SwapQueue()==0)
先判斷消費者隊列是否為空,不為空就不會繼續后面的判斷了,也就不會進行交互隊列。
只有消費者隊列為空才會進行交換。SwapQueue()會返回交換后的隊列有多少任務 ==0說明隊列為空(但生產者隊列有數據才會交換 沒有數據就會阻塞,除非要退出 會設置為非阻塞)
,所以交換完隊列還為空就需要返回false進行退出。
交換時 處理消費者需要加鎖,生產者也需要加鎖。
等生產者隊列有數據再進行交換,或者退出設為非阻塞