std::thread執行緒庫詳解(3)

目錄

前言

前兩篇的博文分別介紹了標準庫裡面的執行緒和鎖,這一次的博文將會介紹鎖的管理。

鎖在多執行緒編程中非常常用,但是一旦使用不謹慎就會導致很多問題,最常見的就是死鎖問題。

lock_guard

std::lock_guard是最常見的管理鎖的類,它會在初始化的時候自動加鎖,銷毀的時候自動解鎖,需要鎖的對象滿足BasicLockable,即存在lockunlock方法。測試程式碼:

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_tdefer_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;
}

由於其允許在未加鎖構造,所以它也提供了相應的locktry_lockunlock等方法。

shared_lock

std::unique_lock類似,不過這個是鎖定讀寫鎖的讀部分。

總結

本文總結了標準庫中所有的鎖管理的類,合理使用可以使程式碼更優美。這是標準庫執行緒第三篇博文了,第四篇將會介紹執行緒裡面的條件變數。

ref

[1] //zh.wikipedia.org/wiki/哲學家就餐問題

部落格原文://www.cnblogs.com/ink19/p/std_thread-3.html

Tags:
Exit mobile version