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);  	/*...  	   中間用來寫需要加鎖的內容  	*/    }