std::unique_lock 使用場景 1

std::unique_lock 是一種可以轉移所有權 (move constructor and move assignment) 的智慧指標 (smart pointer)。

假設在需要 thread-safe 的情況下,有一個大函式做了很多事,如類別成員函式 void BigFunc()。

class LockDemo
{
public:
	void BigFunc()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		// prepare data
		// process data
		// clean data
	}

private:
	std::mutex m_mutex;
};

經過分析,發現此函式做了三件事 prepare data、process data 及 clean data。而根據單一職責原則 (Single Responsibility Principle),我們希望一個函式只做一件事,因此我們將此函式拆分為三個意義明確的小函式 (PrepareData, ProcessData and CleanData),並且 BigFunc只是轉呼叫此三個函式。

class LockDemo
{
public:
	void BigFunc()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		PrepareData();
		ProcessData();
		CleanData();
	}

private:
	void PrepareData()
	{
		// ...
	}

	void ProcessData()
	{
		// ...
	}

	void CleanData()
	{
		// ...
	}

private:
	std::mutex m_mutex;
};

又如果我們希望此三個函式可以由 caller 自由決定呼叫組合,我們會將此三個函式從 private function 改為 public function。

但如此一來,為了保證 thread-safe,必須將此三個函式都加上 std::unique_lock,而同一條 thread 鎖住自己兩次會造成 deadlock,因此 BigFunc 的使用就會有問題。

class LockDemo
{
public:
	void BigFunc()
	{
		std::unique_lock<std::mutex> lock(m_mutex); // lock 一次
		PrepareData(); // 呼叫此函式會再 lock 一次,造成 deadlock
		ProcessData();
		CleanData();
	}

public:
	void PrepareData()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		// ...
	}

	void ProcessData()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		// ...
	}

	void CleanData()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		// ...
	}

private:
	std::mutex m_mutex;
};

看來為了解決這個問題,我們只能將 BigFunc 的 std::unique_lock 移除。
P.S. 這裡不考慮使用 recursive_mutex,因為使用 recursive_mutex 通常表示設計上有問題,這會在別篇做解釋。

然而稍微不幸的是,如果在非常重視效率的情況下,此重構方式會讓呼叫 BigFunc 從建構及解構一次 std::unique_lock 變成了三次,而這可能是無法接受的效率損失,那麼還有其他方法嗎?有的,我們可以再次修改如下:

class LockDemo
{
public:
	std::unique_lock<std::mutex> GetLock()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		return lock;
	}

public:
	void BigFunc()
	{
		auto lock = GetLock();
		CleanData(ProcessData(PrepareData(std::move(lock))));
	}

public:
	std::unique_lock<std::mutex> PrepareData(std::unique_lock<std::mutex> lock)
	{

		// ...
		return lock;
	}

	std::unique_lock<std::mutex> ProcessData(std::unique_lock<std::mutex> lock)
	{
		// ...
		return lock;
	}

	std::unique_lock<std::mutex> CleanData(std::unique_lock<std::mutex> lock)
	{
		// ...
		return lock;
	}

private:
	std::mutex m_mutex;
};

利用 std::unique_lock 支援 move constructor,使用所有權轉移 (move constructor) 來取代建構子 (constructor) 呼叫,且解構子 (destructor) 的呼叫成本也有所降低 (因為是所有權轉移,中間的解構子呼叫不會執行 unlock)。

當然,此方法將使 caller 的呼叫變得複雜,另一個可以重構的方向是同時提供 non-thread-safe 及 thread-safe 版本的類別或函式,而這可以帶出裝飾者模式 (Decorate pattern) 的使用,將會在別篇介紹。

發佈留言