c++多線程基礎3(mutex)
互斥鎖
互斥算法避免多個線程同時訪問共享資源。這會避免數據競爭,並提供線程間的同步支持。定義於頭文件 <mutex>
互斥鎖有可重入、不可重入之分。C++標準庫中用 mutex 表示不可重入的互斥鎖,用 recursive_mutex 表示可重入的互斥鎖。為這兩個類增加根據時間來阻塞線程的能力,就又有了兩個新的互斥鎖:timed_mutex(不可重入的鎖)、recursive_timed_mutex(可重入的鎖)
C++標準庫的所有mutex都是不可拷貝的,也不可移動
std::mutex:
mutex
類是能用於保護共享數據免受從多個線程同時訪問的同步原語。mutex
提供排他性非遞歸所有權語義
lock:如果 mutex 未上鎖,則將其上鎖。否則如果已經其它線程 lock,則阻塞當前線程
try_lock:如果 mutex 未上鎖,則將其上鎖。否則返回 false,並不阻塞當前線程
unlock:如果 mutex 被當前線程鎖住,則將其解鎖。否則,是未定義的行為
native_handle:返回底層實現定義的線程句柄
註意:std::mutex
既不可復制亦不可移動
例1:
1 #include <iostream> 2 #include <chrono> 3 #include <thread> 4 #include <mutex> 5View Codeusing namespace std; 6 7 int g_num = 0;//為 g_num_mutex 所保護 8 std::mutex g_num_mutex; 9 10 void slow_increment(int id) { 11 for(int i = 0; i < 3; ++i) { 12 g_num_mutex.lock(); 13 ++g_num; 14 cout << id << " => " << g_num << endl; 15 g_num_mutex.unlock();16 17 std::this_thread::sleep_for(std::chrono::seconds(1)); 18 } 19 } 20 21 int main(void) { 22 std::thread t1(slow_increment, 0); 23 std::thread t2(slow_increment, 1); 24 t1.join(); 25 t2.join(); 26 27 // 輸出: 28 // 0 => 1 29 // 1 => 2 30 // 0 => 3 31 // 1 => 4 32 // 0 => 5 33 // 1 => 6 34 35 return 0; 36 }
例2:
1 #include <iostream> 2 #include <chrono> 3 #include <mutex> 4 #include <thread> 5 using namespace std; 6 7 std::chrono::milliseconds interval(100); 8 std::mutex mtex; 9 int job_shared = 0;//兩個線程都能修改,mtex將保護此變量 10 int job_exclusive = 0;//只有一個線程能修改 11 12 //此線程能修改 jon_shared 和 job_exclusive 13 void job_1() { 14 std::this_thread::sleep_for(interval);//令job_2持鎖 15 16 while(true) { 17 //嘗試鎖定 mtex 以修改 job_shared 18 if(mtex.try_lock()) { 19 cout << "job shared (" << job_shared << ")\n"; 20 mtex.unlock(); 21 return; 22 } else { 23 //不能修改 job_shared 24 ++job_exclusive; 25 cout << "job exclusive (" << job_exclusive << ")\n"; 26 std::this_thread::sleep_for(interval); 27 } 28 } 29 } 30 31 // 此線程只能修改 job_shared 32 void job_2() { 33 mtex.lock(); 34 std::this_thread::sleep_for(5 * interval); 35 ++job_shared; 36 mtex.unlock(); 37 } 38 39 int main(void) { 40 std::thread t1(job_1); 41 std::thread t2(job_2); 42 t1.join(); 43 t2.join(); 44 45 // 輸出: 46 // job exclusive (1) 47 // job exclusive (2) 48 // job exclusive (3) 49 // job exclusive (4) 50 // job shared (1) 51 52 return 0; 53 }View Code
std::timed_mutex:
timed_mutex
類是能用於保護數據免受多個線程同時訪問的同步原語。
以類似 mutex 的行為, timed_mutex
提供排他性非遞歸所有權語義。另外,timed_mutex 在 mutex 的基礎上增加了以下兩個操作:
try_lock_for():
函數原型:template< class Rep, class Period >
bool try_lock_for( const std::chrono::duration<Rep,Period>& timeout_duration );
嘗試鎖互斥。阻塞直到經過指定的 timeout_duration
或得到鎖,取決於何者先到來。成功獲得鎖時返回 true , 否則返回 false 。若 timeout_duration
小於或等於 timeout_duration.zero()
,則函數表現同 try_lock() 。由於調度或資源爭議延遲,此函數可能阻塞長於 timeout_duration
。
標準推薦用 steady_clock 度量時長。若實現用 system_clock 代替,則等待時間亦可能對時鐘調整敏感。
同 try_lock() ,允許此函數虛假地失敗並返回 false ,即使在 timeout_duration
中某點互斥不為任何線程所鎖定。
若此操作返回 true ,則同一互斥上先前的 unlock() 調用同步於(定義於 std::memory_order )它。若已占有 mutex
的線程調用 try_lock_for
,則行為未定義。
try_lock_until(time_point):
函數原型:template< class Clock, class Duration >
bool try_lock_until( const std::chrono::time_point<Clock,Duration>& timeout_time );
嘗試所互斥。阻塞直至抵達指定的 timeout_time
或得到鎖,取決於何者先到來。成功獲得鎖時返回 true ,否則返回 false 。若已經過 timeout_time
,則此函數表現同 try_lock() 。
使用傾向於 timeout_time
的時鐘,這表示時鐘調節有影響。從而阻塞的最大時長可能小於但不會大於在調用時的 timeout_time - Clock::now() ,依賴於調整的方向。由於調度或資源爭議延遲,函數亦可能阻塞長於抵達 timeout_time
之後。同 try_lock() ,允許此函數虛假地失敗並返回 false ,即使在 timeout_time
前的某點任何線程都不鎖定互斥。
若此操作返回 true ,則同一互斥上先前的 unlock() 調用同步於(定義於 std::memory_order )它。
若已占有 mutex
的線程調用 try_lock_until
,則行為未定義。
try_lock_for / until可以檢測到死鎖的出現:
1 if(!try_lock_for(chrono::hours(1))) 2 { 3 throw "出現死鎖!"; 4 }View Code
例1:
1 #include <iostream> 2 #include <mutex> 3 #include <thread> 4 #include <vector> 5 #include <sstream> 6 7 std::mutex cout_mutex; // 控制到 std::cout 的訪問 8 std::timed_mutex mutex; 9 10 void job(int id) 11 { 12 using Ms = std::chrono::milliseconds; 13 std::ostringstream stream; 14 15 for (int i = 0; i < 3; ++i) { 16 if (mutex.try_lock_for(Ms(100))) { 17 stream << "success "; 18 std::this_thread::sleep_for(Ms(100)); 19 mutex.unlock(); 20 } else { 21 stream << "failed "; 22 } 23 std::this_thread::sleep_for(Ms(100)); 24 } 25 26 std::lock_guard<std::mutex> lock(cout_mutex); 27 std::cout << "[" << id << "] " << stream.str() << "\n"; 28 } 29 30 int main() 31 { 32 std::vector<std::thread> threads; 33 for (int i = 0; i < 4; ++i) { 34 threads.emplace_back(job, i); 35 } 36 37 for (auto& i: threads) { 38 i.join(); 39 } 40 41 // 輸出: 42 // [0] failed failed failed 43 // [3] failed failed success 44 // [2] failed success failed 45 // [1] success failed success 46 47 return 0; 48 }View Code
例2:
1 #include <thread> 2 #include <iostream> 3 #include <chrono> 4 #include <mutex> 5 6 std::timed_mutex test_mutex; 7 8 void f() 9 { 10 auto now=std::chrono::steady_clock::now(); 11 test_mutex.try_lock_until(now + std::chrono::seconds(10)); 12 std::cout << "hello world\n"; 13 } 14 15 int main() 16 { 17 std::lock_guard<std::timed_mutex> l(test_mutex); 18 std::thread t(f); 19 t.join(); 20 21 return 0; 22 }View Code
遞歸鎖:
在同一個線程中連續 lock 兩次 mutex 會產生死鎖:
一般情況下,如果同一個線程先後兩次調用 lock,在第二次調?用時,由於鎖已經被占用,該線程會掛起等待占用鎖的線程釋放鎖,然而鎖正是被自己占用著的,該線程又被掛起而沒有機會釋放鎖,因此 就永遠處於掛起等待狀態了,於是就形成了死鎖(Deadlock):
1 #include<iostream> //std::cout 2 #include<thread> //std::thread 3 #include<mutex> //std::mutex 4 using namespace std; 5 mutex g_mutex; 6 7 void threadfun1() 8 { 9 cout << "enter threadfun1" << endl; 10 // lock_guard<mutex> lock(g_mutex); 11 g_mutex.lock(); 12 cout << "execute threadfun1" << endl; 13 g_mutex.unlock(); 14 } 15 16 void threadfun2() 17 { 18 cout << "enter threadfun2" << endl; 19 // lock_guard<mutex> lock(g_mutex); 20 g_mutex.lock(); 21 threadfun1(); 22 cout << "execute threadfun2" << endl; 23 g_mutex.unlock(); 24 } 25 26 int main() 27 { 28 threadfun2(); //死鎖 29 return 0; 30 } 31 32 // 運行結果: 33 // enter threadfun2 34 // enter threadfun1 35 //就會產生死鎖View Code
此時就需要使用遞歸式互斥量 recursive_mutex 來避免這個問題。recursive_mutex 不會產生上述的死鎖問題,只是是增加鎖的計數,但必須確保你 unlock 和 lock 的次數相同,其他線程才可能鎖這個 mutex:
1 #include<iostream> //std::cout 2 #include<thread> //std::thread 3 #include<mutex> //std::mutex 4 using namespace std; 5 6 recursive_mutex g_rec_mutex; 7 8 void threadfun1() 9 { 10 cout << "enter threadfun1" << endl; 11 lock_guard<recursive_mutex> lock(g_rec_mutex); 12 cout << "execute threadfun1" << endl; 13 } 14 15 void threadfun2() 16 { 17 cout << "enter threadfun2" << endl; 18 lock_guard<recursive_mutex> lock(g_rec_mutex); 19 threadfun1(); 20 cout << "execute threadfun2" << endl; 21 } 22 23 int main() 24 { 25 threadfun2(); //利用遞歸式互斥量來避免這個問題 26 return 0; 27 } 28 // 運行結果: 29 // enter threadfun2 30 // enter threadfun1 31 // execute threadfun1 32 // execute threadfun2View Code
recursive_mutex、recursive_timed_mutex 與對應的 mutex、timed_mutex 操作一致。不同點在於,非遞歸鎖在 lock 或 try_lock 一個已經被當前線程 lock 的鎖時會導致死鎖,而遞歸鎖不會
共享鎖:
std::shared_timed_mutex(c++14起)
shared_mutex
類是能用於保護數據免受多個線程同時訪問的同步原語。與其他促進排他性訪問的互斥類型相反, shared_mutex 擁有二個層次的訪問:
- 共享 - 多個線程能共享同一互斥的所有權。
- 排他性 - 僅一個線程能占有互斥。
共享互斥通常用於多個讀線程能同時訪問同一資源而不導致數據競爭,但只有一個寫線程能訪問的情形:
1 #include <iostream> 2 #include <mutex> // 對於 std::unique_lock 3 #include <shared_mutex> 4 #include <thread> 5 6 class ThreadSafeCounter { 7 public: 8 ThreadSafeCounter() = default; 9 10 // 多個線程/讀者能同時讀計數器的值。 11 unsigned int get() const { 12 std::shared_lock<std::shared_timed_mutex> lock(mutex_);//shared_lock 作用類似於 lock_guard 13 return value_; 14 } 15 16 // 只有一個線程/寫者能增加/寫線程的值。 17 void increment() { 18 std::unique_lock<std::shared_timed_mutex> lock(mutex_); 19 value_++; 20 } 21 22 // 只有一個線程/寫者能重置/寫線程的值。 23 void reset() { 24 std::unique_lock<std::shared_timed_mutex> lock(mutex_); 25 value_ = 0; 26 } 27 28 private: 29 mutable std::shared_timed_mutex mutex_; 30 unsigned int value_ = 0; 31 }; 32 33 int main() { 34 ThreadSafeCounter counter; 35 36 auto increment_and_print = [&counter]() { 37 for (int i = 0; i < 3; i++) { 38 counter.increment(); 39 std::cout << std::this_thread::get_id() << ‘ ‘ << counter.get() << ‘\n‘; 40 41 // 註意:寫入 std::cout 實際上也要由另一互斥同步。省略它以保持示例簡潔。 42 } 43 }; 44 45 std::thread thread1(increment_and_print); 46 std::thread thread2(increment_and_print); 47 48 thread1.join(); 49 thread2.join(); 50 51 // 輸出: 52 // 2 1 53 // 3 2 54 // 2 3 55 // 3 4 56 // 2 5 57 // 3 6 58 59 return 0; 60 }View Code
std::shared_mutex(c++17起)
以類似 timed_mutex 的行為, shared_timed_mutex
提供通過 try_lock_for() 、 try_lock_until() 、 try_lock_shared_for() 、 try_lock_shared_until() 方法,試圖帶時限地要求 shared_timed_mutex
所有權的能力。std::shared_mutex 則恰好相反
通用互斥管理:
定義於頭文件 <mutex>
std::lock_guard:
類 lock_guard
是互斥封裝器,為在作用域塊期間占有互斥提供便利 RAII 風格機制。
創建 lock_guard
對象時,它試圖接收給定互斥的所有權。控制離開創建 lock_guard
對象的作用域時,銷毀 lock_guard
並釋放互斥。
lock_guard
類不可復制
要鎖定的互斥,類型必須滿足基礎可鎖要求
代碼:
1 #include <thread> 2 #include <mutex> 3 #include <iostream> 4 5 int g_i = 0; 6 std::mutex g_i_mutex; // 保護 g_i 7 8 void safe_increment() 9 { 10 std::lock_guard<std::mutex> lock(g_i_mutex); 11 ++g_i; 12 13 std::cout << std::this_thread::get_id() << ": " << g_i << ‘\n‘; 14 15 // g_i_mutex 在鎖離開作用域時自動釋放 16 } 17 18 int main() 19 { 20 std::cout << "main: " << g_i << ‘\n‘; 21 22 std::thread t1(safe_increment); 23 std::thread t2(safe_increment); 24 25 t1.join(); 26 t2.join(); 27 28 std::cout << "main: " << g_i << ‘\n‘; 29 30 // 輸出: 31 // main: 0 32 // 2: 1 33 // 3: 2 34 // main: 2 35 36 return 0; 37 }View Code
c++多線程基礎3(mutex)