1. 程式人生 > >c++多線程基礎3(mutex)

c++多線程基礎3(mutex)

inter 情況下 test opened 互斥鎖 slow clock 否則 val

互斥鎖

互斥算法避免多個線程同時訪問共享資源。這會避免數據競爭,並提供線程間的同步支持。定義於頭文件 <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>
 5
using 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 }
View Code

例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 threadfun2
View 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)