一、實驗介紹
1.1 實驗內容
為了追求性能,在服務器開發中我們經常要面臨大量線程任務之間的調度和管理,本次實驗我們將使用 C++ 設計並實現一個簡單的線程池庫。
本課程介紹需要用到的庫和基本原理
1.2 實驗知識點
-
C++11 標准庫特性
- std::thread
- std::mutex, std::unique_lock
- std::condition_variable
- std::future, std::packaged_task
- std::function, std::bind
- std::shared_ptr, std::make_shared
- std::move, std::forward
-
C++11 語言特性
- Lambda 表達式
- 尾置返回類型
-
線程池模型
- 測試驅動開發思想
1.3 適合人群
適合對於c++想深入學習,對於計算機操作系統想深入學習的同學,雖然代碼不長但是設計的知識點很多,希望同學們慢慢消化。
二、實驗原理
2.1 線程池簡介
多線程技術主要是解決單個處理器單元內多個線程的執行問題,由此誕生了所謂的線程池技術。線程池主要由三個基本部分組成:
- 線程池管理器(Thread Pool):負責創建、管理線程池,最基本的操作為:創建線程池、銷毀線程池、增加新的線程任務;
- 工作線程(Worker):線程池中的線程,在沒有任務時會處於等待狀態,可以循環執行任務;
- 任務隊列(Tasks Queue):未處理任務的緩存隊列。

簡單來說,一個線程池負責管理了需要執行的多個並發執行的多個線程中可執行數量多的線程、以及他們之間的調度。
為了更深刻的理解線程池這項技術,我們來看一個實際點的例子。
在 Web 服務器中,如果一天中服務器需要處理一百萬個請求,並且每個請求都需要讓一個獨立的線程完成。為了保證服務器任務執行的高效性(能執行的趕緊執行),不應該讓並發執行的線程數無節制的增長,所以,線程池在這其中就發揮了作用。
考慮一個處理器完成一項任務會分為創建線程、線程執行任務和銷毀線程三個階段。
如果在某個訪問高峰期同時出現了十萬的並發請求,且每個任務的請求都很簡單,甚至執行任務的時間還小於這個線程被創建的時間,那么這時處理器必須花費大量的時間來創建這些請求的線程,而很長時間內讓各個線程得不到執行。
有了線程池之后,我們可以在程序啟動后創建一定數量的線程,當任務到達后,緩沖隊列會將任務加入到線程中進行執行,執行完成后,線程並不銷毀,而是等待下一任務的到來。
2.2 基礎知識
C++11 引入了非常豐富且有用的新特性,尤其是並發編程支持的大量新特性,這才使得我們能夠在100行以內編寫一個復雜的線程池成為可能。在設計編寫線程池之前,我們先回顧一下我們可能會用到的這些特性。
我們將在接下來的篇幅中復習(學習)下面這些 C++11 的特性、泛型編程以及多線程中的並發模型(互斥鎖),如果對這些內容較熟悉,可以直接跳過本節直接查看下一個實驗:
- 語言特性
- lambda expression
- 尾置返回類型
- 右值引用
- 標准庫特性
- std::thread
- std::mutex, std::unique_lock
- std::future, std::packaged_task
- std::condition_variable
- std::function, std::bind
- std::shared_ptr, std::make_shared
- std::move, std::forward
2.3 語言特性
1. Lambda 表達式
Lambda 表達式是 C++11中最重要的新特性之一,而 Lambda 表達式,實際上就是提供了一個類似匿名函數的特性,而匿名函數則是在需要一個函數,但是又不想費力去命名一個函數的情況下去使用的。這樣的場景其實有很多很多,所以匿名函數幾乎是現代編程語言的標配。
Lambda 表達式的基本語法如下:
[捕獲列表](參數列表) mutable(可選) 異常屬性 -> 返回類型 { // 函數體 }
上面的語法規則除了 [捕獲列表] 內的東西外,其他部分都很好理解,只是一般函數的函數名被略去,返回值使用了一個 -> 的形式進行。
所謂捕獲列表,其實可以理解為參數的一種類型,lambda 表達式內部函數體在默認情況下是不能夠使用函數體外部的變量的,這時候捕獲列表可以起到傳遞外部數據的作用。根據傳遞的行為,捕獲列表也分為以下幾種:
1. 值捕獲
與參數傳值類似,值捕獲的前期是變量可以拷貝,不同之處則在於,被捕獲的變量在 lambda 表達式被創建時拷貝,而非調用時才拷貝:
void learn_lambda_func_1() { int value_1 = 1; auto copy_value_1 = [value_1] { return value_1; }; value_1 = 100; auto stored_value_1 = copy_value_1(); // 這時, stored_value_1 == 1, 而 value_1 == 100. // 因為 copy_value_1 在創建時就保存了一份 value_1 的拷貝 }
2. 引用捕獲
與引用傳參類似,引用捕獲保存的是引用,值會發生變化。
void learn_lambda_func_2() { int value_2 = 1; auto copy_value_2 = [&value_2] { return value_2; }; value_2 = 100; auto stored_value_2 = copy_value_2(); // 這時, stored_value_2 == 100, value_1 == 100. // 因為 copy_value_2 保存的是引用 }
3. 隱式捕獲
手動書寫捕獲列表有時候是非常復雜的,這種機械性的工作可以交給編譯器來處理,這時候可以在捕獲列表中寫一個 & 或 = 向編譯器聲明采用 引用捕獲或者值捕獲.
總結一下,捕獲提供了lambda 表達式對外部值進行使用的功能,捕獲列表的最常用的四種形式可以是:
- [] 空捕獲列表
- [name1, name2, ...] 捕獲一系列變量
- [&] 引用捕獲, 讓編譯器自行推導捕獲列表
- [=] 值捕獲, 讓編譯器執行推導應用列表
2.尾置返回類型
有時候,當希望編寫一個函數來接收某個序列容器中返回的一個元素的應用時候,你可能就不太能夠想明白應該如何寫出這個函數的返回值類型了:
template <typename T> return_type &getItem(T begin, T end) { return *begin; // 返回序列中一個元素的引用 }
這里的 return_type 應該怎么寫呢?事實上,我們可能會想到使用 decltype() 來獲得這個類型,但是,編譯器在讀到這個函數定義的時候,begin 甚至還沒有出現,這時候我們似乎沒有任何辦法直接在返回類型的時候寫下這個返回類型。
C++11 提供了一種新的書寫返回值的方式,那就是將返回類型尾置。尾置的返回類型允許我們在參數列表之后申明返回的類型,我們的代碼可以寫成:
template <typename T> auto &getItem(T begin, T end) -> decltype(*begin) { return *begin; // 返回序列中一個元素的引用 }
其中,我們使用 decltype 告知了編譯器返回類型與參數表中的返回類型相同,而 decltype 會自動推斷為元素類型的引用,完成了我們的需求。
當然,並不是只有這種情況才能夠使用尾置返回類型,任函數都可以這么干,這種寫法的好處在於能夠讓我們的返回類型變得清晰,以至於我們不會被各種復雜的返回類型搞得頭暈,例如:
```cpp
int (*)[5]func(in
本課程為會員專屬,查看完整內容請
