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

留言
張貼留言