std::thread執行緒庫詳解(3)
目錄
前言
前兩篇的博文分別介紹了標準庫裡面的執行緒和鎖,這一次的博文將會介紹鎖的管理。
鎖在多執行緒編程中非常常用,但是一旦使用不謹慎就會導致很多問題,最常見的就是死鎖問題。
lock_guard
std::lock_guard
是最常見的管理鎖的類,它會在初始化的時候自動加鎖,銷毀的時候自動解鎖,需要鎖的對象滿足BasicLockable
,即存在lock
和unlock
方法。測試程式碼:
void thread_func(int thread_id) {
{
std::lock_guard<std::mutex> guard(global_mutex);
std::cout << "Test 1:" << thread_id << std::endl;
std::this_thread::sleep_for(1s);
std::cout << "Test 2:" << thread_id << std::endl;
}
std::this_thread::sleep_for(0.5s);
std::cout << "Test 3:" << thread_id << std::endl;
}
std::lock_guard
在執行緒一開始的程式碼塊就進行了初始化,global_mutex
加鎖,所以Test 1和Test 2會一起輸出,而之後程式碼塊結束,std::lock_guard
作為區域變數,也在此時析構釋放鎖。其輸出為
除此之外,std::lock_guard
還允許輸入第二個參數std::adopt_lock_t
,這個參數表明該執行緒已經獲取了該鎖,所以在創建對象的時候不需要再獲取鎖。
std::lock_guard<std::mutex> lk(mutex1, std::adopt_lock);
scoped_lock (C++17)
這個類和std::lock_guard
類似,不過其可以同時管理多把鎖,可以同時給多把鎖加鎖。這個方法可以很好的解決哲學家就餐問題\(^1\)。
void thread_func(int thread_id, std::mutex &mutex1, std::mutex &mutex2) {
std::scoped_lock lock(mutex1, mutex2);
std::cout << "Thread " << thread_id << " is eating." << std::endl;
std::this_thread::sleep_for(1s);
std::cout << "Thread " << thread_id << " over." << std::endl;
}
std::vector<std::shared_ptr<std::thread>> philosopher;
std::vector<std::mutex> tableware_mutex(5);
for (int loop_i = 0; loop_i < 5; ++loop_i) {
philosopher.push_back(
std::make_shared<std::thread>(thread_func, loop_i, std::ref(tableware_mutex[loop_i]), std::ref(tableware_mutex[(loop_i + 1) % 5]))
);
}
for (int loop_i = 0; loop_i < 5; ++loop_i) {
philosopher.at(loop_i)->join();
}
這裡我們初始化了五個哲學家(執行緒)和五個餐具(鎖),每個哲學家需要兩個相鄰的餐具來進食。其結果很簡單:
可以看到,這幫哲學家們很有序的進食,沒有產生衝突。而如果我們把對應的std::scoped_lock lock(mutex1, mutex2);
,改為兩個std::lock_guard
,肉眼可見的會出現噁心的死鎖問題。
和std::lock_guard
類似的,它也有一個參數std::adopt_lock_t
,表明執行緒已經獲取到鎖,構造時不需要獲取鎖,不過這個參數位於第一個。
std::scoped_lock lk(std::adopt_lock, mutex1, mutex2);
unique_lock
std::unique_lock
相比較與std::lock_guard
更為自由,除了std::adopt_lock_t
參數外,其還支援try_to_lock_t
,defer_lock_t
,其中try_to_lock_t
為非阻塞型加鎖,defer_lock_t
不在初始化的時候加鎖。
std::unique_lock<std::mutex> lk(mutex, std::adopt_lock);
std::unique_lock<std::mutex> lk(mutex, std::try_to_lock);
std::unique_lock<std::mutex> lk(mutex, std::defer_lock);
std::unique_lock
支援超時加鎖:
std::unique_lock<std::timed_mutex> lk(mutex, 1s);
std::unique_lock
支援移動語義,所以可以作為返回值
std::unique_lock<std::mutex> get_lock() {
std::unique_lock<std::mutex> lk(mutex);
return lk;
}
void thread_func(int thread_id) {
std::unique_lock<std::mutex> lk = get_lock();
std::cout << "Test 1: " << thread_id << std::endl;
std::this_thread::sleep_for(1s);
std::cout << "Test 2: " << thread_id << std::endl;
}
由於其允許在未加鎖構造,所以它也提供了相應的lock
、try_lock
、unlock
等方法。
shared_lock
和std::unique_lock
類似,不過這個是鎖定讀寫鎖的讀部分。
總結
本文總結了標準庫中所有的鎖管理的類,合理使用可以使程式碼更優美。這是標準庫執行緒第三篇博文了,第四篇將會介紹執行緒裡面的條件變數。