C++ std::mutex互斥元/鎖

0.前言

將多線程作爲並行的關鍵優點之一,在於它們之間簡單直接地共享數據的能力。但當多個線程修改同一數據時,就很容易因爲競爭而導致錯誤的執行結果。若所有線程只是進行讀取操作,就沒有問題。

1.認識std::mutex

由C++標準提供的保護共享數據的最基本機制是互斥元/鎖(mutex)(讀繆特克斯)。在訪問共享數據之前,鎖定(lock)與該數據相關的互斥元,訪問完成後,解鎖(unlock)該互斥元。線程庫會確保一個線程鎖定某個互斥元后,其他試圖鎖定該互斥元的線程必須等待,直到之前的線程解鎖。

可以通過在線手冊查看該類接口 https://zh.cppreference.com/w/cpp/thread/mutex ,除了lock和unlock,還有一個try_lock方法:嘗試鎖定互斥,若互斥不可用則返回。需要注意的是,不要隨意將受保護數據的指針或引用傳遞到鎖的範圍之外。

#include <mutex>

std::mutex mtx;
int num;

void set(int i) {
	mtx.lock();
	num = i;
	mtx.unlock();
}

int get() {
	mtx.lock();
	const int temp = num;
	mtx.unlock();
	return temp;
}

2.認識std::lock_guard

標準C++庫還提供了std::lock_guard類模板,在構造時鎖定給定的互斥元,析構時將互斥元解鎖,從而保證鎖定的互斥元始終被正確解鎖。

#include <mutex>

std::mutex mtx;
int num;

void set(int i) {
	std::lock_guard<std::mutex> guard(mtx);
	num = i;
}

int get() {
	std::lock_guard<std::mutex> guard(mtx);
	return num;
}

該類模板有兩個構造函數:

<1> explicit lock_guard( mutex_type& m ); 等效地調用 m.lock() ,若 m 不是遞歸互斥,且當前線程已佔有 m 則行爲未定義。

<2> lock_guard( mutex_type& m, std::adopt_lock_t t ); 獲得互斥 m 的所有權而不試圖鎖定它。若當前線程不佔有 m 則行爲未定義。

如果單單使用mutex和lock_guard還是比較簡單的,畢竟接口就那麼幾個。下面的例子中,我構造了一個隊列,一個線程寫入操作,兩個線程讀取操作。

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <queue>
//pair
#include <utility>

template<typename T>
class ThreadQueue
{
public:
	ThreadQueue() {}
	~ThreadQueue() {}

	void inqueue(const T& item) {
		std::lock_guard<std::mutex> guard(dataMutex);
		//dataMutex.lock();
		dataQueue.push(item);
		//dataMutex.unlock();
	}

	std::pair<bool, T> dequeue() {
		std::lock_guard<std::mutex> guard(dataMutex);
		//dataMutex.lock();
		bool is_valid = false;
		T item;
		if (!dataQueue.empty()) {
			is_valid = true;
			item = dataQueue.front();
			dataQueue.pop();
		}
		//dataMutex.unlock();
		return std::make_pair(is_valid, item);
	}

	bool notEmpty() const {
		return (!dataQueue.empty());
	}

private:
	std::mutex dataMutex;
	std::queue<T> dataQueue;
};

int main()
{
	ThreadQueue<int> my_queue;
	int counter = 0;
	bool working = true;
	std::thread write_thread([&]() {
		while (working) {
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
			my_queue.inqueue(++counter);
			std::cerr << "write:" << counter << std::endl;
		}
		});
	std::thread read_thread1([&]() {
		while (working) {
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
			if (my_queue.notEmpty()) {
				std::pair<bool, int> item = my_queue.dequeue();
				if (item.first)
					std::cerr << "read 1:" << item.second << std::endl;
			}
		}
		});
	std::thread read_thread2([&]() {
		while (working) {
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
			if (my_queue.notEmpty()) {
				std::pair<bool, int> item = my_queue.dequeue();
				if (item.first)
					std::cerr << "read 2:" << item.second << std::endl;
			}
		}
		});

	getchar();
	working = false;

	write_thread.join();
	read_thread1.join();
	read_thread2.join();

	system("pause");
	return 0;
}

3.死鎖

在設計互斥元的編程時,也要注意很多問題,如死鎖和鎖定粒度大小等。如果對於一個給定的操作需要鎖定兩個或者更多的互斥元,很容易存在死鎖,每個線程都擁有一個互斥元,同時又在等待另外一個釋放。爲了避免死鎖,通常建議始終使用相同的順序鎖定這兩個互斥元,如果總是鎖定B之前鎖定A,那麼永遠不會死鎖。但這並不能滿足所有的應用場景。

對於需要同時鎖定多個互斥元的場景,可以使用標準庫的std::lock函數,該函數可以同時鎖定兩個或多個互斥元,而不用擔心死鎖。

#include <mutex>

struct Data
{
	int value;
	std::mutex mtx;
};

void value_swap(Data &a, Data &b)
{
	if (&a == &b)
		return;
	std::lock(a.mtx, b.mtx);
	//額外的sdt::adopt_lock參數表示互斥元已鎖定,
	//只是沿用互斥元所有權,而不是在構造中鎖定互斥元
	std::lock_guard<std::mutex> lock_a(a.mtx, std::adopt_lock);
	std::lock_guard<std::mutex> lock_b(a.mtx, std::adopt_lock);
	std::swap(a.value, b.value);
}

《C++併發編程實戰》第三章給出了一些避免死鎖的建議:

  • 避免嵌套鎖;
  • 持有鎖時,避免調用用戶提供的代碼;
  • 以固定順序獲取鎖;
  • 使用鎖層次;
  • 將這些設計準則擴展到鎖之外:死鎖不只是出現於鎖定中,他可以發生在任何可以導致循環等待的同步結構中。

4.認識std::unique_lock

類 unique_lock 是通用互斥包裝器,允許延遲鎖定、鎖定的有時限嘗試(time-constrained attempts at locking)、遞歸鎖定、所有權轉移以及和條件變量一起使用。unique_lock比lock_guard提供了更多的靈活性,但性能更低一點。支持移動語義,但不支持拷貝。

std::unique_lock能夠被傳遞給std::lock,因爲std::unique_lock提供了lock/unlock/try_lock三個成員函數,它們會轉發給底層互斥元上的同名成員函數去執行。

這裏借用下在線手冊上的例子:

#include <mutex>
#include <thread>
#include <chrono>

struct Box {
	explicit Box(int num) : num_things{ num } {}

	int num_things;
	std::mutex m;
};

void transfer(Box& from, Box& to, int num)
{
	// 仍未實際取鎖
	std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
	std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);

	// 鎖兩個 unique_lock 而不死鎖
	std::lock(lock1, lock2);

	from.num_things -= num;
	to.num_things += num;

	// 'from.m' 與 'to.m' 互斥解鎖於 'unique_lock' 析構函數
}

int main()
{
	Box acc1(100);
	Box acc2(50);

	std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
	std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);

	t1.join();
	t2.join();
}

對於該類可以查看手冊,以及去網上找一些更復雜的使用實例。

本文記錄的是《C++併發編程實戰》第三章前半部分的內容,後面的一些內容我打算單獨寫學習記錄,這樣學的更深入點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章