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) 的使用,將會在別篇介紹。