C++ 11 併發控制(鎖)
在 《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 的建構函式。
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 (相關推薦
C++ 11 併發控制(鎖)
在 《C++11 併發指南三(std::mutex 詳解)》一文中我們主要介紹了 C++11 標準中的互斥量(Mutex),並簡單介紹了一下兩種鎖型別。本節將詳細介紹一下 C++11 標準的鎖型別。 C++11 標準為我們提供了兩種基本的鎖型別,分別如下: std::
C++11併發程式設計(一)——初始C++11多執行緒庫
1 前言 C++11標準在標準庫中為多執行緒提供了元件,這意味著使用C++編寫與平臺無關的多執行緒程式成為可能,而C++程式的可移植性也得到了有力的保證。 在之前我們主要使用的多執行緒庫要麼
C++11新特性(一)
auto關鍵字 C語言中其實就有auto關鍵字,修飾可變化的量,但是由於平時我們直接使用int a = 10;也是宣告變數,編譯器已經自動幫我們加上了auto關鍵字,是C語言中應用最廣泛的一種型別,也就是說,省去型別說明符auto的都是自動變數! 隨著時代進步,
C++11新特性(66)- 用static_cast將左值轉換為右值
溫故而知新 本文涉及兩個概念,static_cast和右值引用,在閱讀本文之前,最好先閱讀下面的文章。 使用std::move 考察下面兩個函式: 除了引數型別一個是左值引用,一個是右值引用以外都一樣。結合前面的文章可以得出下面的結論:左值引用表明這個
C++11新特性(1)-long long
溫故而知新迄今為止的職業生涯中,有過兩次集中時間學習C++的經歷。第一次大概是在1994年前後,那時非計算機專業大學畢業剛接觸C++,學的是還是BorlandC++3.1的手冊。許多東西都是一知半解就開始了應用,但即使是這樣,還是充分感覺到C++的強大,非常喜歡C++帶來的那
C++11新特性(2)- 列表初始化
以前什麼樣C或者C++在初始化陣列時,可以使用下面的花括號加初始值的形式:intint_array[]={1,2,3,4,5};在C++中,如果有下面這樣一個類:classTester{public:Tester(intvalue):m_value(value*2){}voi
C++11新特性(51)- 移動建構函式通常應該是noexcept
不會丟擲異常的移動建構函式 拷貝建構函式通常伴隨著記憶體分配操作,因此很可能會丟擲異常;移動建構函式一般是移動記憶體的所有權,所以一般不會丟擲異常。 C++11中新引入了一個noexcept關鍵字,用來向程式設計師,編譯器來表明這種情況。 noexc
併發程式設計(三): 使用C++11實現無鎖stack(lock-free stack)
C++11中CAS實現: template< class T> struct atomic { public: bool compare_exchange_weak( T& expected, T desired,
C++11併發學習之四:執行緒同步(續)
有時候,在第一個執行緒完成前,可能需要等待另一個執行緒執行完成。C++標準庫提供了一些工具可用於這種同步操作,形式上表現為條件變數(condition variable)和期望(future)。 一.條件變數(condition variable) C++標準庫對條件變數有兩套實現:std::c
C++11 併發與多執行緒篇(未完成)
從C++11新標準開始,C++語言本身增加了對多執行緒的支援,意味著使用C++可實現多執行緒程式的可移植,跨平臺。 在標準的C++程式中,主執行緒從main()開始執行,我們自己在C++中建立的執行緒,也需要從一個函式開始執行(這個函式叫做初始函式),一旦這個函式執行完
C++11 併發程式設計基礎(一):併發、並行與C++多執行緒
正文 C++11標準在標準庫中為多執行緒提供了元件,這意味著使用C++編寫與平臺無關的多執行緒程式成為可能,而C++程式的可移植性也得到了有力的保證。另外,併發程式設計可提高應用的效能,這對對效能錙銖必較的C++程式設計師來說是值得關注的。 回到頂部 1. 何為併發 併發指的是兩個或多個獨立的活動在同
C++11併發/多執行緒程式設計系列(2)
std::thread詳解 std::thread在標頭檔案<thread>中宣告,因此使用 std::thread 時需要包含 <thread>標頭檔案。 default(1) thread() noexcept;
C++11 併發與多執行緒(二)
1)執行緒間共享資料 執行緒間共享資料的問題 原因:由於修改資料引起,如果都只是讀資料,沒有任何問題; 競爭條件: 例子:電影院同時買熱門電影票,只剩幾個位置 **data r
C# 《五》流程控制(1)
generic ram div 執行 進行 運行 align strong c# 1、分支語句之 if 語句 1、流程控制語句是程序的核心部分,對任何一門編程語言來說都至關重要,是控制程序執行流向的基本語句。如果一門語言缺少了流程控制,就會缺少對程序流向的控制,就不能稱之為
Python之進程同步控制(鎖信號量事件 )、進程間通信——隊列和管道
load 很快 容器 數據安全 全部 傳遞 幫我 之前 引入 進程同步(multiprocess.Lock、multiprocess.Semaphore、multiprocess.Event) 鎖 —— multiprocess.Lock 通過剛剛的學習,我們千方百計實現了
C++快速入門---訪問控制(12)
C++快速入門---訪問控制(12) 訪問控制:C++提供了一種用來保護類裡的方法和屬性的手段。 這裡所說的保護意思是對誰可以呼叫某個方法和訪問某個屬性加上一個限制。如果某個物件試圖呼叫一個它無權訪問的函式,編譯器將報錯。 C++的訪問級別:
Java併發程式設計(1):可重入內建鎖
每個Java物件都可以用做一個實現同步的鎖,這些鎖被稱為內建鎖或監視器鎖。執行緒在進入同步程式碼塊之前會自動獲取鎖,並且在退出同步程式碼塊時會自動釋放鎖。獲得內建鎖的唯一途徑就是進入由這個鎖保護的同步程式碼塊或方法。 當某個執行緒請求一個由其他執行緒持有的鎖時,發出請求的執行緒就會阻塞。然而,由於內建鎖是可
Java併發程式設計(7):使用synchronized獲取互斥鎖的幾點說明
在併發程式設計中,多執行緒同時併發訪問的資源叫做臨界資源,當多個執行緒同時訪問物件並要求操作相同資源時,分割了原子操作就有可能出現數據的不一致或資料不完整的情況,為避免這種情況的發生,我們會採取同步機制,以確保在某一時刻,方法內只允許有一個執行緒。 採用synchronized修飾符實現的同步機制叫做互斥鎖
Java併發程式設計(6)- J.U.C元件拓展
J.U.C-FutureTask 在Java中一般通過繼承Thread類或者實現Runnable介面這兩種方式來建立執行緒,但是這兩種方式都有個缺陷,就是不能在執行完成後獲取執行的結果,因此Java 1.5之後提供了Callable和Future介面,通過它們就可以在任務執行完畢之後得到任務的執行結果。
Java併發程式設計(9):死鎖(含程式碼)
JAVA大資料中高階架構 2018-11-10 14:04:32當執行緒需要同時持有多個鎖時,有可能產生死鎖。考慮如下情形: 執行緒A當前持有互斥所鎖lock1,執行緒B當前持有互斥鎖lock2。接下來,當執行緒A仍然持有lock1時,它試圖獲取lock2,因為執行緒B正持有lock2,因此執行緒A會阻塞等