C++11 併發指南三(Lock 詳解)
在 《C++11 併發指南三(std::mutex 詳解)》一文中我們主要介紹了 C++11 標準中的互斥量(Mutex),並簡單介紹了一下兩種鎖型別。本節將詳細介紹一下 C++11 標準的鎖型別。
C++11 標準為我們提供了兩種基本的鎖型別,分別如下:
- std::lock_guard,與 Mutex RAII 相關,方便執行緒對互斥量上鎖。
- std::unique_lock,與 Mutex RAII 相關,方便執行緒對互斥量上鎖,但提供了更好的上鎖和解鎖控制。
另外還提供了幾個與鎖型別相關的 Tag 類,分別如下:
std::adopt_lock_t,一個空的標記類,定義如下:
struct adopt_lock_t {};
該型別的常量物件adopt_lock(adopt_lock 是一個常量物件,定義如下:
constexpr adopt_lock_t adopt_lock {};,// constexpr 是 C++11 中的新關鍵字)
通常作為引數傳入給 unique_lock 或 lock_guard 的建構函式。
std::
defer_lock_t
,一個空的標記類,定義如下:
struct defer_lock_t {};
該型別的常量物件
defer_lock
(defer_lock
是一個常量物件,定義如下:
constexpr defer_lock_t defer_lock {};,// constexpr 是 C++11 中的新關鍵字)
通常作為引數傳入給 unique_lock 或 lock_guard 的建構函式。
std::
try_to_lock_t
,一個空的標記類,定義如下:
struct try_to_lock_t {};
該型別的常量物件
try_to_lock
(try_to_lock
是一個常量物件,定義如下:
constexpr try_to_lock_t try_to_lock {};,// constexpr 是 C++11 中的新關鍵字)
通常作為引數傳入給 unique_lock 或 lock_guard 的建構函式。
後面我們會詳細介紹以上三種 Tag 型別在配合 lock_gurad 與 unique_lock 使用時的區別。
std::lock_guard 介紹
std::lock_gurad 是 C++11 中定義的模板類。定義如下:
template <class Mutex> class lock_guard;
lock_guard 物件通常用於管理某個鎖(Lock)物件,因此與 Mutex RAII 相關,方便執行緒對互斥量上鎖,即在某個 lock_guard 物件的宣告週期內,它所管理的鎖物件會一直保持上鎖狀態;而 lock_guard 的生命週期結束之後,它所管理的鎖物件會被解鎖(注:類似 shared_ptr 等智慧指標管理動態分配的記憶體資源 )。
模板引數 Mutex 代表互斥量型別,例如 std::mutex 型別,它應該是一個基本的 BasicLockable 型別,標準庫中定義幾種基本的 BasicLockable 型別,分別 std::mutex, std::recursive_mutex, std::timed_mutex,std::recursive_timed_mutex (以上四種類型均已在上一篇部落格中介紹)以及 std::unique_lock(本文後續會介紹 std::unique_lock)。(注:BasicLockable 型別的物件只需滿足兩種操作,lock 和 unlock,另外還有 Lockable 型別,在 BasicLockable 型別的基礎上新增了 try_lock 操作,因此一個滿足 Lockable 的物件應支援三種操作:lock,unlock 和 try_lock;最後還有一種 TimedLockable 物件,在 Lockable 型別的基礎上又新增了 try_lock_for 和 try_lock_until 兩種操作,因此一個滿足 TimedLockable 的物件應支援五種操作:lock, unlock, try_lock, try_lock_for, try_lock_until)。
在 lock_guard 物件構造時,傳入的 Mutex 物件(即它所管理的 Mutex 物件)會被當前執行緒鎖住。在lock_guard 物件被析構時,它所管理的 Mutex 物件會自動解鎖,由於不需要程式設計師手動呼叫 lock 和 unlock 對 Mutex 進行上鎖和解鎖操作,因此這也是最簡單安全的上鎖和解鎖方式,尤其是在程式丟擲異常後先前已被上鎖的 Mutex 物件可以正確進行解鎖操作,極大地簡化了程式設計師編寫與 Mutex 相關的異常處理程式碼。
值得注意的是,lock_guard 物件並不負責管理 Mutex 物件的生命週期,lock_guard 物件只是簡化了 Mutex 物件的上鎖和解鎖操作,方便執行緒對互斥量上鎖,即在某個 lock_guard 物件的宣告週期內,它所管理的鎖物件會一直保持上鎖狀態;而 lock_guard 的生命週期結束之後,它所管理的鎖物件會被解鎖。
std::lock_guard 建構函式
lock_guard 建構函式如下表所示:
locking (1) |
explicit lock_guard (mutex_type& m); |
---|---|
adopting (2) |
lock_guard (mutex_type& m, adopt_lock_t tag); |
copy [deleted](3) |
lock_guard (const lock_guard&) = delete; |
- locking 初始化
- lock_guard 物件管理 Mutex 物件 m,並在構造時對 m 進行上鎖(呼叫 m.lock())。
- adopting初始化
- lock_guard 物件管理 Mutex 物件 m,與 locking 初始化(1) 不同的是, Mutex 物件 m 已被當前執行緒鎖住。
- 拷貝構造
- lock_guard 物件的拷貝構造和移動構造(move construction)均被禁用,因此 lock_guard 物件不可被拷貝構造或移動構造。
我們來看一個簡單的例子(參考):
#include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::lock_guard, std::adopt_lock std::mutex mtx; // mutex for critical section void print_thread_id (int id) { mtx.lock(); std::lock_guard<std::mutex> lck(mtx, std::adopt_lock); std::cout << "thread #" << id << '\n'; } int main () { std::thread threads[10]; // spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(print_thread_id,i+1); for (auto& th : threads) th.join(); return 0; }
在 print_thread_id 中,我們首先對 mtx 進行上鎖操作(mtx.lock();),然後用 mtx 物件構造一個 lock_guard 物件(std::lock_guard<std::mutex> lck(mtx, std::adopt_lock);),注意此時 Tag 引數為 std::adopt_lock,表明當前執行緒已經獲得了鎖,此後 mtx 物件的解鎖操作交由 lock_guard 物件 lck 來管理,在 lck 的生命週期結束之後,mtx 物件會自動解鎖。
lock_guard 最大的特點就是安全易於使用,請看下面例子(參考),在異常丟擲的時候通過 lock_guard 物件管理的 Mutex 可以得到正確地解鎖。
#include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::lock_guard #include <stdexcept> // std::logic_error std::mutex mtx; void print_even (int x) { if (x%2==0) std::cout << x << " is even\n"; else throw (std::logic_error("not even")); } void print_thread_id (int id) { try { // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception: std::lock_guard<std::mutex> lck (mtx); print_even(id); } catch (std::logic_error&) { std::cout << "[exception caught]\n"; } } int main () { std::thread threads[10]; // spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(print_thread_id,i+1); for (auto& th : threads) th.join(); return 0; }
std::unique_lock 介紹
但是 lock_guard 最大的缺點也是簡單,沒有給程式設計師提供足夠的靈活度,因此,C++11 標準中定義了另外一個與 Mutex RAII 相關類 unique_lock,該類與 lock_guard 類相似,也很方便執行緒對互斥量上鎖,但它提供了更好的上鎖和解鎖控制。
顧名思義,unique_lock 物件以獨佔所有權的方式( unique owership)管理 mutex 物件的上鎖和解鎖操作,所謂獨佔所有權,就是沒有其他的 unique_lock 物件同時擁有某個 mutex 物件的所有權。
在構造(或移動(move)賦值)時,unique_lock 物件需要傳遞一個 Mutex 物件作為它的引數,新建立的 unique_lock 物件負責傳入的 Mutex 物件的上鎖和解鎖操作。
std::unique_lock 物件也能保證在其自身析構時它所管理的 Mutex 物件能夠被正確地解鎖(即使沒有顯式地呼叫 unlock 函式)。因此,和 lock_guard 一樣,這也是一種簡單而又安全的上鎖和解鎖方式,尤其是在程式丟擲異常後先前已被上鎖的 Mutex 物件可以正確進行解鎖操作,極大地簡化了程式設計師編寫與 Mutex 相關的異常處理程式碼。
值得注意的是,unique_lock 物件同樣也不負責管理 Mutex 物件的生命週期,unique_lock 物件只是簡化了 Mutex 物件的上鎖和解鎖操作,方便執行緒對互斥量上鎖,即在某個 unique_lock 物件的宣告週期內,它所管理的鎖物件會一直保持上鎖狀態;而 unique_lock 的生命週期結束之後,它所管理的鎖物件會被解鎖,這一點和 lock_guard 類似,但 unique_lock 給程式設計師提供了更多的自由,我會在下面的內容中給大家介紹 unique_lock 的用法。
另外,與 lock_guard 一樣,模板引數 Mutex 代表互斥量型別,例如 std::mutex 型別,它應該是一個基本的 BasicLockable 型別,標準庫中定義幾種基本的 BasicLockable 型別,分別 std::mutex, std::recursive_mutex, std::timed_mutex,std::recursive_timed_mutex (以上四種類型均已在上一篇部落格中介紹)以及 std::unique_lock(本文後續會介紹 std::unique_lock)。(注:BasicLockable 型別的物件只需滿足兩種操作,lock 和 unlock,另外還有 Lockable 型別,在 BasicLockable 型別的基礎上新增了 try_lock 操作,因此一個滿足 Lockable 的物件應支援三種操作:lock,unlock 和 try_lock;最後還有一種 TimedLockable 物件,在 Lockable 型別的基礎上又新增了 try_lock_for 和 try_lock_until 兩種操作,因此一個滿足 TimedLockable 的物件應支援五種操作:lock, unlock, try_lock, try_lock_for, try_lock_until)。
std::unique_lock 建構函式
std::unique_lock 的建構函式的數目相對來說比 std::lock_guard 多,其中一方面也是因為 std::unique_lock 更加靈活,從而在構造 std::unique_lock 物件時可以接受額外的引數。總地來說,std::unique_lock 建構函式如下:
default (1) |
unique_lock() noexcept; |
---|---|
locking (2) |
explicit unique_lock(mutex_type& m); |
try-locking (3) |
unique_lock(mutex_type& m, try_to_lock_t tag); |
deferred (4) |
unique_lock(mutex_type& m, defer_lock_t tag) noexcept; |
adopting (5) |
unique_lock(mutex_type& m, adopt_lock_t tag); |
locking for (6) |
template <class Rep, class Period> unique_lock(mutex_type& m, const chrono::duration<Rep,Period>& rel_time); |
locking until (7) |
template <class Clock, class Duration> unique_lock(mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time); |
copy [deleted] (8) |
unique_lock(const unique_lock&) = delete; |
move (9) |
unique_lock(unique_lock&& x); |
下面我們來分別介紹以上各個建構函式:
- (1) 預設建構函式
- 新建立的 unique_lock 物件不管理任何 Mutex 物件。
- (2) locking 初始化
- 新建立的 unique_lock 物件管理 Mutex 物件 m,並嘗試呼叫 m.lock() 對 Mutex 物件進行上鎖,如果此時另外某個 unique_lock 物件已經管理了該 Mutex 物件 m,則當前執行緒將會被阻塞。
- (3) try-locking 初始化
- 新建立的 unique_lock 物件管理 Mutex 物件 m,並嘗試呼叫 m.try_lock() 對 Mutex 物件進行上鎖,但如果上鎖不成功,並不會阻塞當前執行緒。
- (4) deferred 初始化
- 新建立的 unique_lock 物件管理 Mutex 物件 m,但是在初始化的時候並不鎖住 Mutex 物件。 m 應該是一個沒有當前執行緒鎖住的 Mutex 物件。
- (5) adopting 初始化
- 新建立的 unique_lock 物件管理 Mutex 物件 m, m 應該是一個已經被當前執行緒鎖住的 Mutex 物件。(並且當前新建立的 unique_lock 物件擁有對鎖(Lock)的所有權)。
- (6) locking 一段時間(duration)
- 新建立的 unique_lock 物件管理 Mutex 物件 m,並試圖通過呼叫 m.try_lock_for(rel_time) 來鎖住 Mutex 物件一段時間(rel_time)。
- (7) locking 直到某個時間點(time point)
- 新建立的 unique_lock 物件管理 Mutex 物件m,並試圖通過呼叫 m.try_lock_until(abs_time) 來在某個時間點(abs_time)之前鎖住 Mutex 物件。
- (8) 拷貝構造 [被禁用]
- unique_lock 物件不能被拷貝構造。
- (9) 移動(move)構造
- 新建立的 unique_lock 物件獲得了由 x 所管理的 Mutex 物件的所有權(包括當前 Mutex 的狀態)。呼叫 move 構造之後, x 物件如同通過預設建構函式所建立的,就不再管理任何 Mutex 物件了。
綜上所述,由 (2) 和 (5) 建立的 unique_lock 物件通常擁有 Mutex 物件的鎖。而通過 (1) 和 (4) 建立的則不會擁有鎖。通過 (3),(6) 和 (7) 建立的 unique_lock 物件,則在 lock 成功時獲得鎖。
關於unique_lock 的建構函式,請看下面例子(參考):
#include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::lock, std::unique_lock // std::adopt_lock, std::defer_lock std::mutex foo,bar; void task_a () { std::lock (foo,bar); // simultaneous lock (prevents deadlock) std::unique_lock<std::mutex> lck1 (foo,std::adopt_lock); std::unique_lock<std::mutex> lck2 (bar,std::adopt_lock); std::cout << "task a\n"; // (unlocked automatically on destruction of lck1 and lck2) } void task_b () { // foo.lock(); bar.lock(); // replaced by: std::unique_lock<std::mutex> lck1, lck2; lck1 = std::unique_lock<std::mutex>(bar,std::defer_lock); lck2 = std::unique_lock<std::mutex>(foo,std::defer_lock); std::lock (lck1,lck2); // simultaneous lock (prevents deadlock) std::cout << "task b\n"; // (unlocked automatically on destruction of lck1 and lck2) } int main () { std::thread th1 (task_a); std::thread th2 (task_b); th1.join(); th2.join(); return 0; }
std::unique_lock 移動(move assign)賦值操作
std::unique_lock 支援移動賦值(move assignment),但是普通的賦值被禁用了,
move (1) |
unique_lock& operator= (unique_lock&& x) noexcept; |
---|---|
copy [deleted] (2) |
unique_lock& operator= (const unique_lock&) = delete; |
移動賦值(move assignment)之後,由 x 所管理的 Mutex 物件及其狀態將會被新的 std::unique_lock 物件取代。
如果被賦值的物件之前已經獲得了它所管理的 Mutex 物件的鎖,則在移動賦值(move assignment)之前會呼叫 unlock 函式釋放它所佔有的鎖。
呼叫移動賦值(move assignment)之後, x 物件如同通過預設建構函式所建立的,也就不再管理任何 Mutex 物件了。請看下面例子(參考):
#include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock std::mutex mtx; // mutex for critical section void print_fifty (char c) { std::unique_lock<std::mutex> lck; // default-constructed lck = std::unique_lock<std::mutex>(mtx); // move-assigned for (int i=0; i<50; ++i) { std::cout << c; } std::cout << '\n'; } int main () { std::thread th1 (print_fifty,'*'); std::thread th2 (print_fifty,'$'); th1.join(); th2.join(); return 0; }
std::unique_lock 主要成員函式
本節我們來看看 std::unique_lock 的主要成員函式。由於 std::unique_lock 比 std::lock_guard 操作靈活,因此它提供了更多成員函式。具體分類如下:
- 上鎖/解鎖操作:lock,try_lock,try_lock_for,try_lock_until 和 unlock
- 修改操作:移動賦值(move assignment)(前面已經介紹過了),交換(swap)(與另一個 std::unique_lock 物件交換它們所管理的 Mutex 物件的所有權),釋放(release)(返回指向它所管理的 Mutex 物件的指標,並釋放所有權)
- 獲取屬性操作:owns_lock(返回當前 std::unique_lock 物件是否獲得了鎖)、operator bool()(與 owns_lock 功能相同,返回當前 std::unique_lock 物件是否獲得了鎖)、mutex(返回當前 std::unique_lock 物件所管理的 Mutex 物件的指標)。
std::unique_lock::lock請看下面例子(參考):
上鎖操作,呼叫它所管理的 Mutex 物件的 lock 函式。如果在呼叫 Mutex 物件的 lock 函式時該 Mutex 物件已被另一執行緒鎖住,則當前執行緒會被阻塞,直到它獲得了鎖。
該函式返回時,當前的 unique_lock 物件便擁有了它所管理的 Mutex 物件的鎖。如果上鎖操作失敗,則丟擲 system_error 異常。
// unique_lock::lock/unlock #include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock, std::defer_lock std::mutex mtx; // mutex for critical section void print_thread_id (int id) { std::unique_lock<std::mutex> lck (mtx,std::defer_lock); // critical section (exclusive access to std::cout signaled by locking lck): lck.lock(); std::cout << "thread #" << id << '\n'; lck.unlock(); } int main () { std::thread threads[10]; // spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(print_thread_id,i+1); for (auto& th : threads) th.join(); return 0; }
std::unique_lock::try_lock
上鎖操作,呼叫它所管理的 Mutex 物件的 try_lock 函式,如果上鎖成功,則返回 true,否則返回 false。
請看下面例子(參考):
#include <iostream> // std::cout #include <vector> // std::vector #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock, std::defer_lock std::mutex mtx; // mutex for critical section void print_star () { std::unique_lock<std::mutex> lck(mtx,std::defer_lock); // print '*' if successfully locked, 'x' otherwise: if (lck.try_lock()) std::cout << '*'; else std::cout << 'x'; } int main () { std::vector<std::thread> threads; for (int i=0; i<500; ++i) threads.emplace_back(print_star); for (auto& x: threads) x.join(); return 0; }
std::unique_lock::try_lock_for
上鎖操作,呼叫它所管理的 Mutex 物件的 try_lock_for 函式,如果上鎖成功,則返回 true,否則返回 false。
請看下面例子(參考):
#include <iostream> // std::cout #include <chrono> // std::chrono::milliseconds #include <thread> // std::thread #include <mutex> // std::timed_mutex, std::unique_lock, std::defer_lock std::timed_mutex mtx; void fireworks () { std::unique_lock<std::timed_mutex> lck(mtx,std::defer_lock); // waiting to get a lock: each thread prints "-" every 200ms: while (!lck.try_lock_for(std::chrono::milliseconds(200))) { std::cout << "-"; } // got a lock! - wait for 1s, then this thread prints "*" std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "*\n"; } int main () { std::thread threads[10]; // spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(fireworks); for (auto& th : threads) th.join(); return 0; }
std::unique_lock::try_lock_until
上鎖操作,呼叫它所管理的 Mutex 物件的 try_lock_for 函式,如果上鎖成功,則返回 true,否則返回 false。
請看下面例子(參考):
#include <iostream> // std::cout #include <chrono> // std::chrono::milliseconds #include <thread> // std::thread #include <mutex> // std::timed_mutex, std::unique_lock, std::defer_lock std::timed_mutex mtx; void fireworks () { std::unique_lock<std::timed_mutex> lck(mtx,std::defer_lock); // waiting to get a lock: each thread prints "-" every 200ms: while (!lck.try_lock_for(std::chrono::milliseconds(200))) { std::cout << "-"; } // got a lock! - wait for 1s, then this thread prints "*" std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "*\n"; } int main () { std::thread threads[10]; // spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(fireworks); for (auto& th : threads) th.join(); return 0; }
std::unique_lock::unlock
解鎖操作,呼叫它所管理的 Mutex 物件的 unlock 函式。
請看下面例子(參考):
#include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock, std::defer_lock std::mutex mtx; // mutex for critical section void print_thread_id (int id) { std::unique_lock<std::mutex> lck (mtx,std::defer_lock); // critical section (exclusive access to std::cout signaled by locking lck): lck.lock(); std::cout << "thread #" << id << '\n'; lck.unlock(); } int main () { std::thread threads[10]; // spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(print_thread_id,i+1); for (auto& th : threads) th.join(); return 0; }
std::unique_lock::release
返回指向它所管理的 Mutex 物件的指標,並釋放所有權。
請看下面例子(參考):
#include <iostream> // std::cout #include <vector> // std::vector #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock std::mutex mtx; int count = 0; void print_count_and_unlock (std::mutex* p_mtx) { std::cout << "count: " << count << '\n'; p_mtx->unlock(); } void task() { std::unique_lock<std::mutex> lck(mtx); ++count; print_count_and_unlock(lck.release()); } int main () { std::vector<std::thread> threads; for (int i=0; i<10; ++i) threads.emplace_back(task); for (auto& x: threads) x.join(); return 0; }
std::unique_lock::owns_lock
返回當前 std::unique_lock 物件是否獲得了鎖。
請看下面例子(參考):
#include <iostream> // std::cout #include <vector> // std::vector #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock, std::try_to_lock std::mutex mtx; // mutex for critical section void print_star () { std::unique_lock<std::mutex> lck(mtx,std::try_to_lock); // print '*' if successfully locked, 'x' otherwise: if (lck.owns_lock()) std::cout << '*'; else std::cout << 'x'; } int main () { std::vector<std::thread> threads; for (int i=0; i<500; ++i) threads.emplace_back(print_star); for (auto& x: threads) x.join(); return 0; }
std::unique_lock::operator bool()
與 owns_lock 功能相同,返回當前 std::unique_lock 物件是否獲得了鎖。
請看下面例子(參考):
#include <iostream> // std::cout #include <vector> // std::vector #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock, std::try_to_lock std::mutex mtx; // mutex for critical section void print_star () { std::unique_lock<std::mutex> lck(mtx,std::try_to_lock); // print '*' if successfully locked, 'x' otherwise: if (lck) std::cout << '*'; else std::cout << 'x'; } int main () { std::vector<std::thread> threads; for (int i=0; i<500; ++i) threads.emplace_back(print_star); for (auto& x: threads) x.join(); return 0; }
std::unique_lock::mutex
返回當前 std::unique_lock 物件所管理的 Mutex 物件的指標。
請看下面例子(參考):
#include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock, std::defer_lock class MyMutex : public std::mutex { int _id; public: MyMutex (int id) : _id(id) {} int id() {return _id;} }; MyMutex mtx (101); void print_ids (int id) { std::unique_lock<MyMutex> lck (mtx); std::cout << "thread #" << id << " locked mutex " << lck.mutex()->id() << '\n'; } int main () { std::thread threads[10]; // spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(print_ids,i+1); for (auto& th : threads) th.join(); return 0; }
好了,本文先介紹到這裡,我們基本上介紹完了 C++11 多執行緒程式設計中兩種最基本的鎖型別,後面我會繼續更新有關 C++11 併發程式設計的部落格,希望感興趣的同學繼續關注 ;-)