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,一個空的標記類,定義如下:
| structadopt_lock_t {}; |
?該類型的常量對象adopt_lock(adopt_lock 是一個常量對象,定義如下:
?| constexpradopt_lock_t adopt_lock {};,// constexpr 是 C++11 中的新關鍵字) |
通常作為參數傳入給 unique_lock 或 lock_guard 的構造函數。
- std::defer_lock_t,一個空的標記類,定義如下:
| structdefer_lock_t {}; |
?該類型的常量對象 defer_lock(defer_lock 是一個常量對象,定義如下:
?| constexprdefer_lock_t defer_lock {};,// constexpr 是 C++11 中的新關鍵字) |
通常作為參數傳入給 unique_lock 或 lock_guard 的構造函數。
- std::try_to_lock_t,一個空的標記類,定義如下:
| structtry_to_lock_t {}; |
?該類型的常量對象 try_to_lock(try_to_lock 是一個常量對象,定義如下:
?| constexprtry_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 構造函數如下表所示:
| explicit lock_guard (mutex_type& m); |
| lock_guard (mutex_type& m, adopt_lock_t tag); |
| lock_guard (const lock_guard&) = delete; |
- lock_guard 對象管理 Mutex 對象 m,并在構造時對 m 進行上鎖(調用 m.lock())。
- 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 sectionvoid 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 構造函數如下:
| unique_lock() noexcept; |
| explicit unique_lock(mutex_type& m); |
| unique_lock(mutex_type& m, try_to_lock_t tag); |
| unique_lock(mutex_type& m, defer_lock_t tag) noexcept; |
| unique_lock(mutex_type& m, adopt_lock_t tag); |
| template <class Rep, class Period> unique_lock(mutex_type& m, const chrono::duration<Rep,Period>& rel_time); |
| template <class Clock, class Duration> unique_lock(mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time); |
| unique_lock(const unique_lock&) = delete; |
| unique_lock(unique_lock&& x); |
下面我們來分別介紹以上各個構造函數:
(1) 默認構造函數綜上所述,由 (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),但是普通的賦值被禁用了,
| unique_lock& operator= (unique_lock&& x) noexcept; |
| unique_lock& operator= (const unique_lock&) = delete; |
移動賦值(move assignment)之后,由 x 所管理的 Mutex 對象及其狀態將會被新的 std::unique_lock 對象取代。
如果被賦值的對象之前已經獲得了它所管理的 Mutex 對象的鎖,則在移動賦值(move assignment)之前會調用 unlock 函數釋放它所占有的鎖。
調用移動賦值(move assignment)之后, x 對象如同通過默認構造函數所創建的,也就不再管理任何 Mutex 對象了。請看下面例子(參考):
std::unique_lock 主要成員函數
本節我們來看看 std::unique_lock 的主要成員函數。由于 std::unique_lock 比 std::lock_guard 操作靈活,因此它提供了更多成員函數。具體分類如下:
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 sectionvoid 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 sectionvoid 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 sectionvoid 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 sectionvoid 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 sectionvoid 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_lockclass 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 并發編程的博客,希望感興趣的同學繼續關注 ;-)
總結
以上是生活随笔為你收集整理的C++11 并发指南三(Lock 详解)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 28、shareSDK分享以及 QQ应用
- 下一篇: C++11 并发指南二(std::thr