c++11 新特性實戰 (一)
c++11多線程操作
-
線程
- thread
int main() { thread t1(Test1); t1.join(); thread t2(Test2); t2.join(); thread t3 = t1; thread t4(t1); thread t5 = std::move(t1); thread t6(std::move(t1)); return 0; }
t3,t4創建失敗,因爲thread的拷貝構造和賦值運算符重載的原型是:
thread(const thread&) = delete; thread& operator=(const thread&) = delete;
被禁用了,但是t5, t6線程是創建成功的。std::move把t1轉換爲右值,調用的是函數原型爲
thread& operator=(thread&& _Other) noexcept
和thread(thread&& _Other) noexcept
。當線程對象t1被移動拷貝和移動賦值給t5和t6的時候,t1就失去了線程控制權,也就是一個線程只能同時被一個線程對象所控制。最直觀的是t1.joinable()返回值爲false,joinable()函數後面介紹。
使用類成員函數作爲線程參數:
class Task { public: Task(){} void Task1() {} void Task2() {} private: }; int main() { Task task; thread t3(&Task::Task1, &task); t3.join(); return 0; }
關鍵點是要創建一個類對象,並作爲第二個參數傳入
thread()
線程的構造函數中去。 -
管理當前線程的函數
- yield
此函數的準確性爲依賴於實現,特別是使用中的 OS 調度器機制和系統狀態。例如,先進先出實時調度器( Linux 的
SCHED_FIFO
)將懸掛當前線程並將它放到準備運行的同優先級線程的隊列尾(而若無其他線程在同優先級,則yield
無效果)。#include <iostream> #include <chrono> #include <thread> // 建議其他線程運行一小段時間的“忙睡眠” void little_sleep(std::chrono::microseconds us) { auto start = std::chrono::high_resolution_clock::now(); auto end = start + us; do { std::this_thread::yield(); } while (std::chrono::high_resolution_clock::now() < end); } int main() { auto start = std::chrono::high_resolution_clock::now(); little_sleep(std::chrono::microseconds(100)); auto elapsed = std::chrono::high_resolution_clock::now() - start; std::cout << "waited for " << std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count() << " microseconds\n"; }
- get_id
這個函數不用過多介紹了,就是用來獲取當前線程id的,用來標識線程的身份。
std::thread::id this_id = std::this_thread::get_id();
- sleep_for
位於this_thread命名空間下,msvc下支持兩種時間參數。
std::this_thread::sleep_for(2s); std::this_thread::sleep_for(std::chrono::seconds(1));
- sleep_untile
參數構建起來挺麻煩的,一般場景下要求線程睡眠的就用sleep_for就行了
using std::chrono::system_clock; time_t tt = system_clock::to_time_t(system_clock::now()); struct std::tm *ptm = localtime(&tt); std::this_thread::sleep_until(system_clock::from_time_t(mktime(ptm)));
-
互斥
- mutex
對於互斥量看到一個很好的比喻:
單位上有一臺打印機(共享數據a),你要用打印機(線程1要操作數據a),同事老王也要用打印機(線程2也要操作數據a),但是打印機同一時間只能給一個人用,此時,規定不管是誰,在用打印機之前都要向領導申請許可證(lock),用完後再向領導歸還許可證(unlock),許可證總共只有一個,沒有許可證的人就等着在用打印機的同事用完後才能申請許可證(阻塞,線程1lock互斥量後其他線程就無法lock,只能等線程1unlock後,其他線程才能lock),那麼,這個許可證就是互斥量。互斥量保證了使用打印機這一過程不被打斷。
代碼示例:
mutex mtx; int gNum = 0; void Test1() { mtx.lock(); for(int n = 0; n < 5; ++n) gNum++; mtx.unlock(); } void Test2() { std::cout << "gNum = " << gNum << std::endl; } int main() { thread t1(Test1); t1.join(); thread t2(Test2); t2.join(); return 0; }
join()表示主線程等待子線程結束再繼續執行,如果我們的期望是打印循環自增之後的gNum的值,那t1.join()就放在t2創建之前調用。因爲t2的創建就標誌着t2線程創建好然後開始執行了。
通常mutex不單獨使用,因爲lock和unlock必須配套使用,如果忘記unlock很可能造成死鎖,即使unlock寫了,但是如果在執行之前程序捕獲到異常,也還是一樣會死鎖。如何解決使用mutex造成的死鎖問題呢?下面介紹unique_gard和lock_guard的時候詳細說明。
- timed_mutex
std::mutex cout_mutex; // 控制到 std::cout 的訪問 std::timed_mutex mutex; void job(int id) { using Ms = std::chrono::milliseconds; std::ostringstream stream; for (int i = 0; i < 3; ++i) { if (mutex.try_lock_for(Ms(100))) { stream << "success "; std::this_thread::sleep_for(Ms(100)); mutex.unlock(); } else { stream << "failed "; } std::this_thread::sleep_for(Ms(100)); } std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "[" << id << "] " << stream.str() << "\n"; } int main() { std::vector<std::thread> threads; for (int i = 0; i < 4; ++i) { threads.emplace_back(job, i); } for (auto& i: threads) { i.join(); } }
這裏的第28行衍生出一個知識點:STL的
emplace_back
函數。這是c++11新增的容器類的操作函數,如果第二個參數忽略,用法和push_back
相似,都是在stl後面追加元素。函數原型:template<class... _Valty> decltype(auto) emplace_back(_Valty&&... _Val)
是一個變長的模板函數,例子中的代碼傳遞的是一個函數指針job,
emplace_back
的實現會把job傳遞給std::thread
的構造函數,與push_back
需要是std::thread類型作爲參數不同,所以emplace_back
是直接在容器中構造了要添加的元素,省去了再次把參數拷貝到stl中的過程,效率更高。提供互斥設施,實現有時限鎖定
- recursive_mutex
提供能被同一線程遞歸鎖定的互斥設施
- recursive_timed_mutex
提供能被同一線程遞歸鎖定的互斥設施,並實現有時限鎖定
-
通用互斥管理
- lock_guard
void Test1() { std::lock_guard<std::mutex> lg(mtx); for(int n = 0; n < 5; ++n) { gNum++; std::cout << "gNum = " << gNum << std::endl; } } int main() { thread t1(Test1); thread t2(Test1); t1.join(); t2.join(); return 0; }
lock_guard相當於利用RAII機制(“資源獲取就是初始化”)把mutex封裝了一下,在構造中lock,在析構中unlock。避免了中間過程出現異常導致的mutex不能夠正常unlock.
- scoped_lock(c++17)
- unique_lock
- defer_lock_t
- try_to_lock_t
- adopt_lock_t
- defer_lock
- try_to_lock
- adopt_lock
-
通用鎖算法
- try_lock
- lock
-
單次調用
- once_flag
- call_once
-
條件變量
- condition_variable
- condition_variable_any
- notify_all_at_thread_exit
- cv_status
-
Future
- promise
- packaged_task
- future
- shared_future
- async
- launch
- future_status
- Future錯誤
- future_error
- future_category
- future_errc