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


整理自:zh.cppreference.com/w/cpp/thread

互斥鎖

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

函數原型:templateclass Rep, class Period >
bool try_lock_forconst 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):

函數原型:templateclass Clock, class Duration >
bool try_lock_untilconst 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

 

std::scoped_lock(c++17起):

類 scoped_lock 是提供便利 RAII 風格機制的互斥包裝器,它在作用域塊的存在期間占有一或多個互斥。

創建 scoped_lock 對象時,它試圖取得給定互斥的所有權。控制離開創建 scoped_lock 對象的作用域時,析構 scoped_lock 並以逆序釋放互斥。若給出數個互斥,則使用免死鎖算法,如同以 std::lock 。

scoped_lock 類不可復制

要鎖的互斥類型必須滿足可鎖要求

構造函數:

explicit scoped_lock( MutexTypes&... );

scoped_lockstd::adopt_lock_t, MutexTypes&... );

 

scoped_lockconst scoped_lock= delete;

取得給定互斥 m 的所有權。

1) 若 sizeof...(MutexTypes== 0 則不做任何事。否則若 sizeof...(MutexTypes== 1 ,則等效地調用 m.lock() 。否則,等效地調用 std::lock(m...) 。若 MutexTypes 之一不是遞歸互斥,且當前線程已占有 m... 中對應的參數,則行為未定義。
2) 取得互斥 m... 的所有權而試圖不鎖定任何互斥。若當前線程已占有 m... 中所有互斥,則行為未定義。
3) 復制構造函數被刪除。

若 m 在 scoped_lock 對象之前被銷毀,則行為未定義

要獲得其所有權的互斥

代碼:

 1 #include <mutex>
 2 #include <thread>
 3 #include <iostream>
 4 #include <vector>
 5 #include <functional>
 6 #include <chrono>
 7 #include <string>
 8  
 9 struct Employee {
10     Employee(std::string id) : id(id) {}
11     std::string id;
12     std::vector<std::string> lunch_partners;
13     std::mutex m;
14     std::string output() const
15     {
16         std::string ret = "Employee " + id + " has lunch partners: ";
17         for( const auto& partner : lunch_partners )
18             ret += partner + " ";
19         return ret;
20     }
21 };
22  
23 void send_mail(Employee &, Employee &)
24 {
25     // 模擬耗時的發信操作
26     std::this_thread::sleep_for(std::chrono::seconds(1));
27 }
28  
29 void assign_lunch_partner(Employee &e1, Employee &e2)
30 {
31     static std::mutex io_mutex;
32     {
33         std::lock_guard<std::mutex> lk(io_mutex);
34         std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
35     }
36  
37     {
38         // 用 std::scoped_lock 取得二個鎖,而無需擔心
39         // 其他對 assign_lunch_partner 的調用死鎖我們
40         // 而且它亦提供便利的 RAII 風格機制
41  
42         std::scoped_lock lock(e1.m, e2.m);
43  
44         // 等價代碼 1 (用 std::lock 和 std::lock_guard )
45         // std::lock(e1.m, e2.m);
46         // std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
47         // std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
48  
49         // 等價代碼 2 (若需要 unique_lock ,例如對於條件變量)
50         // std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
51         // std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
52         // std::lock(lk1, lk2);
53         {
54             std::lock_guard<std::mutex> lk(io_mutex);
55             std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
56         }
57         e1.lunch_partners.push_back(e2.id);
58         e2.lunch_partners.push_back(e1.id);
59     }
60  
61     send_mail(e1, e2);
62     send_mail(e2, e1);
63 }
64  
65 int main()
66 {
67     Employee alice("alice"), bob("bob"), christina("christina"), dave("dave");
68  
69     // 在並行線程中指派,因為就午餐指派發郵件消耗很長時間
70     std::vector<std::thread> threads;
71     threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
72     threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
73     threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
74     threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));
75  
76     for (auto &thread : threads) thread.join();
77     std::cout << alice.output() << '\n'  << bob.output() << '\n'
78               << christina.output() << '\n' << dave.output() << '\n';
79 }
View Code

blog.csdn.net/zouxinfox/article/details/5848519

 

unique_lock

類 unique_lock 可移動,但不可復制——它滿足可移動構造 (MoveConstructible) 和可移動賦值 (MoveAssignable) 但不滿足可復制構造 (CopyConstructible) 或可復制賦值 (CopyAssignable) 。類 unique_lock 是通用互斥包裝器,允許延遲鎖定、鎖定的有時限嘗試、遞歸鎖定、所有權轉移和與條件變量一同使用。

類 unique_lock 滿足基礎可鎖 (BasicLockable) 要求。若 Mutex 滿足可鎖 (Lockable) 要求,則 unique_lock 亦滿足可鎖 (Lockable) 要求(例如:能用於 std::lock ) ;若 Mutex 滿足定時可鎖 (TimedLockable) 要求,則 unique_lock 亦滿足定時可鎖 (TimedLockable) 要求

要鎖定的互斥類型。類型必須滿足基礎可鎖 (BasicLockable) 要求

代碼:

 1 #include <mutex>
 2 #include <thread>
 3 #include <chrono>
 4  
 5 struct Box {
 6     explicit Box(int num) : num_things{num} {}
 7  
 8     int num_things;
 9     std::mutex m;
10 };
11  
12 void transfer(Box &from, Box &to, int num)
13 {
14     // 仍未實際取鎖
15     std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
16     std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
17  
18     // 鎖兩個 unique_lock 而不死鎖
19     std::lock(lock1, lock2);
20  
21     from.num_things -= num;
22     to.num_things += num;
23  
24     // 'from.m' 與 'to.m' 互斥解鎖於 'unique_lock' 析構函數
25 }
26  
27 int main()
28 {
29     Box acc1(100);
30     Box acc2(50);
31  
32     std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
33     std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
34  
35     t1.join();
36     t2.join();
37 }
View Code

 

關於 lock_guard 和 unique_lock:

http://lib.csdn.net/article/cplusplus/29099

http://blog.csdn.net/daaikuaichuan/article/details/71022824


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM