C++ 多線程互斥鎖(mutex,lock,lock_guard)
- 2020 年 2 月 14 日
- 筆記
對於互斥鎖我們要先知道為什麼要用互斥鎖?它能解決什麼問題?
根據這兩個問題,可以來舉個例子說明一下,假如現在我們要求1-10000的和,然後我們為了提高效率,我們建立兩個線程同時去計算[1,5000)的和以及[5000,10001)的和,那麼用於計算和的變量都用相同的ans來獲取結果,代碼如下:
#include <iostream> #include <thread> void work1(int& sum) { for (int i = 1; i < 5000; i++) { sum += i; } } void work2(int& sum) { for (int i = 5000; i <= 10000; i++) { sum += i; } } int fun() { int sum = 0; for (int i = 1; i <= 10000; i++) { sum += i; } return sum; } int main() { int ans = 0; std::thread t1(work1, std::ref(ans)); std::thread t2(work2, std::ref(ans)); t1.join(); t2.join(); std::cout << "sum1 : " << ans << std::endl; std::cout << "sum2 : " << fun() << std::endl; return 0; }
為了區別多線程的計算結果,我用fun函數求的結果與其作比較,然後運行的結果如下圖所示:
我們發現兩次的運算結果並不相同,那麼我們可以分析一下原因,因為在計算過程中的sum是一個引用,是他們的共享資源,所以當一個線程正在計算+i的時候,此時還沒有運算結束,就被切到了另一個線程中,然後在這個線程中可能會計算了很多次+i的操作,然後再切回那個線程中時,計算結果可能就會覆蓋掉另一個線程的計算結果,因此這樣求出來的數一定是比正確結果要小的,所以為了避免這種情況的發生,引入了互斥鎖。
互斥鎖的重點在於他是一個鎖,簡單來說就是我們用鎖將兩個線程中計算過程分別用mutex鎖上,那麼當一個線程正在計算的時候,另一個線程就會等待這個計算的完成。大致流程是這樣的,當work1準備計算sum+=i的時候,用mutex將線程其鎖上,如果此時sum+=i還沒有計算完就切到了work2的線程時,就會通過mutex檢測到已經被鎖上了,那麼work2就會在此等待work1的計算完成,當work1的計算完成以後就會把鎖解開,然後進行下一步的計算。所以兩個線程種的計算過程都是加鎖-計算-解鎖的過程,這樣就不會出現上述所說的那種情況了。
互斥鎖的實現過程很簡單,mutex是一個類,首先我們要先創建出類對象std::mutex mylock,然後在你需要鎖的代碼塊前後加上mylock.lock()和mylock.unlock(),就可以實現互斥鎖的加鎖和解鎖了。可以具體實現可以看下面的代碼:
#include <iostream> #include <thread> #include <mutex> void work1(int& sum, std::mutex& mylock) { for (int i = 1; i < 5000; i++) { mylock.lock(); sum += i; mylock.unlock(); } } void work2(int& sum, std::mutex& mylock) { for (int i = 5000; i <= 10000; i++) { mylock.lock(); sum += i; mylock.unlock(); } } int fun() { int sum = 0; for (int i = 1; i <= 10000; i++) { sum += i; } return sum; } int main() { std::mutex mylock; int ans = 0; std::thread t1(work1, std::ref(ans), std::ref(mylock)); std::thread t2(work2, std::ref(ans), std::ref(mylock)); t1.join(); t2.join(); std::cout << "sum1 : " << ans << std::endl; std::cout << "sum2 : " << fun() << std::endl; return 0; }
這是第一種互斥鎖的實現方法。還有一種是用lock_guard類模板,它的內部結構很簡單,只有構造函數和析構函數,所以也很容里理解它的工作原理,在實例化對象時通過構造函數實現了lock,在析構函數中實現了unlock的操作。這樣就可以避免忘記unlock的情況,具體的實現看下面的代碼:
#include <iostream> #include <thread> #include <mutex> void work1(int& sum, std::mutex& mylock) { for (int i = 1; i < 5000; i++) { std::lock_guard<std::mutex> mylock_guard(mylock); sum += i; } } void work2(int& sum, std::mutex& mylock) { for (int i = 5000; i <= 10000; i++) { std::lock_guard<std::mutex> mylock_guard(mylock); sum += i; } } int fun() { int sum = 0; for (int i = 1; i <= 10000; i++) { sum += i; } return sum; } int main() { std::mutex mylock; int ans = 0; std::thread t1(work1, std::ref(ans), std::ref(mylock)); std::thread t2(work2, std::ref(ans), std::ref(mylock)); t1.join(); t2.join(); std::cout << "sum1 : " << ans << std::endl; std::cout << "sum2 : " << fun() << std::endl; return 0; }
這樣就在每次循環一次後會自動的構建互斥鎖對象,循環完了就會析構掉這個互斥鎖。當然為了使用的更靈活方便,我們可以通過大括號來規定實現的範圍。比如下面這樣:
{ std::lock_guard<std::mutex> mylockguard(mylock); /*... 中間用來寫需要加鎖的內容 */ }