1. 程式人生 > >std::thread執行緒庫詳解(2)

std::thread執行緒庫詳解(2)

## 目錄 - [目錄](#目錄) - [簡介](#簡介) - [最基本的鎖 std::mutex](#最基本的鎖-stdmutex) - [使用](#使用) - [方法和屬性](#方法和屬性) - [遞迴鎖 std::recursive_mutex](#遞迴鎖-stdrecursive_mutex) - [共享鎖 std::shared_mutex (C++17)](#共享鎖-stdshared_mutex-c17) - [帶超時的鎖](#帶超時的鎖) - [總結](#總結) ## 簡介 上一篇博文中,介紹了一下如何建立一個執行緒,分別是`std::thread`和`std::jthread (C++20)`。這兩種方法相似,`std::jthread`相對來說,更加方便一些,具體可以再看看原來的博文,[std::thread執行緒詳解(1)](https://www.cnblogs.com/ink19/p/std_thread-1.html)。 這一次,我將介紹一下,多執行緒的鎖。鎖在多執行緒中是使用非常廣泛的。是多執行緒中最常見的同步方式。主要介紹的鎖有`mutex`,`recursive_mutex`, `shared_mutex`。 ## 最基本的鎖 std::mutex ### 使用 `std::mutex`是最基本的鎖,也是最常見的鎖。它提供了最基本的多執行緒程式設計同步方法。 ```c++ using namespace std::chrono_literals; std::mutex g_mutex; void thread_func() { g_mutex.lock(); std::cout << "Thread out 1: " << std::this_thread::get_id() << std::endl;; std::this_thread::sleep_for(1s); std::cout << "Thread out 2: " << std::this_thread::get_id() << std::endl;; g_mutex.unlock(); } int main() { std::cout << "Mutex Test." << std::endl; std::thread thread1(thread_func); std::thread thread2(thread_func); thread1.join(); thread2.join(); return 0; } ``` 以上示例中,只有一個執行緒函式`thread_func`,它的工作很簡單: 首先對`g_mutex`加鎖,然後輸出一段字串,接著休眠1s,輸出第二段字串,最後對`g_mutex`進行解鎖。 輸出結果如下: ![mutex輸出](https://img2020.cnblogs.com/blog/2105008/202101/2105008-20210110193602128-14009391.png) 鎖的本質是解決多執行緒對同一資源競爭讀寫的問題。這裡我們的資源是標準輸出`std::cout`。鎖的存在讓輸出有序,可預測了。 ### 方法和屬性 - `lock()` 為物件加鎖,如果已經被鎖了,則阻塞執行緒; - `try_lock()` 嘗試加鎖,如果已經被加鎖,則返回false,否則將對其進行加鎖並返回true; - `unlock()` 為物件解鎖,通常和加鎖(`lock()`,`try_lock()`)成對出現; - `native_handle()` 返回鎖的POSIX標準物件。 ## 遞迴鎖 std::recursive_mutex `std::recursive_mutex`是一個遞迴鎖,方法和使用都和`std::mutex`類似。唯一的不同是,`std::mutex`在同一時間,只允許加鎖一次,而`std::revursive_mutex`允許同一執行緒下進行多次加鎖。如: ```c++ // 定義遞迴鎖 std::recursive_mutex g_mutex; // 執行緒函式 void thread_func(int thread_id, int time) { g_mutex.lock(); std::cout << "Thread " << thread_id << ": " << time << std::endl; if (time != 0) thread_func(thread_id, time - 1); g_mutex.unlock(); } // 初始化執行緒 std::thread thread1(thread_func, 1, 3); std::thread thread2(thread_func, 2, 4); ``` 這一次的方法和之前的略有不同,為了更加直觀的觀察不同的執行緒,這次是在輸入的時候輸入一個標誌來區分不同的執行緒。可以清楚的看到,這是一個遞迴函式,每次呼叫的時候都將time減少1,直到其變為0。需要注意的是,在遞迴的時候並沒有釋放鎖,而是直接進入,因此在第二層遍歷的時候,又會對`g_mutex`進行一次加鎖,如果是普通的鎖,次數將會阻塞程序,變成死鎖。但是此時使用的是遞迴鎖,它允許在同一個執行緒,多次加鎖,因此這個程式可以成功執行,並獲得輸出。 ![遞迴鎖輸出](https://img2020.cnblogs.com/blog/2105008/202101/2105008-20210110193602042-1486260280.png) 遞迴鎖的方法和普通鎖的方法類似。 ## 共享鎖 std::shared_mutex (C++17) `std::shared_mutex`在C++14已經存在了,但是在C++14中的`std::shared_mutex`是帶timing的版本的讀寫鎖(也就是說,C++14中的`std::shared_mutex`等於C++17中的`std::shared_timed_mutex`)。讀寫鎖有兩種加鎖的方式,一種是`shared_lock()`,另一種`lock()`。`shared_lock`是讀模式,而`lock`是寫模式。讀寫鎖允許多個讀加鎖,而寫加鎖和其他所有加鎖互斥。即同一時間下: - 允許多個執行緒同時讀; - 只允許一個執行緒寫; - 寫的時候不允許讀,讀的時候不允許寫。 示例: ```c++ // 共享鎖 std::shared_mutex g_mutex; // 讀執行緒 1 void thread_read_1_func(int thread_id) { // 第一個獲取讀許可權 g_mutex.lock_shared(); std::cout << "Read thread " << thread_id << " out 1." << std::endl; // 睡眠2s,等待讀執行緒2,獲取讀許可權,確認可以多個執行緒進行讀加鎖 std::this_thread::sleep_for(2s); std::cout << "Read thread " << thread_id << " out 2." << std::endl; // 解鎖讀 g_mutex.unlock_shared(); } void thread_read_2_func(int thread_id) { // 睡眠500ms,確保讀執行緒1先獲取鎖 std::this_thread::sleep_for(500ms); g_mutex.lock_shared(); std::cout << "Read thread " << thread_id << " out 1." << std::endl; std::this_thread::sleep_for(3s); std::cout << "Read thread " << thread_id << " out 2." << std::endl; g_mutex.unlock_shared(); } void thread_write_1_func(int thread_id) { // 確保讀執行緒先獲得鎖,確認讀寫互斥 std::this_thread::sleep_for(300ms); g_mutex.lock(); std::cout << "Write thread " << thread_id << " out 1." << std::endl; g_mutex.unlock(); } ``` 其輸出為: ![讀寫鎖輸出](https://img2020.cnblogs.com/blog/2105008/202101/2105008-20210110193602127-1357185723.png) ## 帶超時的鎖 上面介紹的所有的鎖,都帶有超時版本。即`timed_mutex`,`recursive_timed_mutex`,`shared_timed_mutex`。他們使用時,和普通版本類似,不過`try_lock`方法多了兩個超時的版本`try_lock_for`和`try_lock_until`。呼叫這一函式時,如果鎖已經被獲取了,執行緒將會阻塞一段時間,如果這一段時間內,獲取到了鎖則返回`true`,否則返回`false` 這裡我們只介紹`timed_mutex`,其他的類似。 ```C++ void thread_func(int thread_id) { if (!g_mutex.try_lock_for(0.5s)) return; std::cout << "Thread out 1: " << thread_id << std::endl;; std::this_thread::sleep_for(1s); std::cout << "Thread out 2: " << thread_id << std::endl;; g_mutex.unlock(); g_mutex.native_handle(); } ``` 其輸出為: ![超時鎖輸出](https://img2020.cnblogs.com/blog/2105008/202101/2105008-20210110193602032-402676263.png) 可以看到,這裡只有一個執行緒有輸出,另一個執行緒,在等待0.5s後直接退出了(沒有獲取到鎖)。 ## 總結 本文主要介紹了三種不同的鎖,普通鎖,遞迴鎖,讀寫鎖。三個鎖有著不一樣的使用方法,但是可以確定的是,過多的使用鎖,會導致程式中的序列部分過多,並行效果不好。因此對於鎖的使用,需要儘量的剋制,儘量的合理。 下一篇文章將介紹鎖的管理。 部落格原文:https://www.cnblogs.com/ink19/p/std_thread