multithreading 程式的小技巧

fcamel
fcamel的程式開發心得
6 min readNov 11, 2016

零星的記錄一些近年來的心得。範例程式都是以 C++ 為例,但概念可以跨不同語言使用。之後有想到什麼再補。

Shared 和 Weak Pointer

跨 thread 的物件用 shared pointer 管理生命週期比較安全,作錯挺多 memory leak,但不會用到 dangle pointer 然後發生無法預期的行為 (程式當下直接掛掉算是幸運的)。

再輔以 weak pointer 減少造成 memory leak 的機會。比方說用 weak pointer存 observer pattern 裡的 observers,避免 observers 影響到物件的生命週期。

Lock

用 lock 保護會跨 thread 寫入的資料。記得都用 scope lock 來使用 lock,避免忘了釋放 lock (比方說不小心有 early return 或是產生 exception),下面是 C++11 的例子:

{
std::lock_guard<std::mutex> lock(m_mutex);
// Do something ...
}

m_mutex 是一個 member field,型別為 std::mutex。

對一個類別來說,還抓不準整個類別用一個 lock 還是將資料分組,不同組用不同的 lock。後者減少 lock contention,不過之後改程式的時候就複雜了一點。

先複製到區域變數再使用

比方說 class 有個 member field 是 shared_ptr<Foo> m_foo。m_foo 有可能被設為 nullptr。為避免使用 m_foo 的途中 m_foo 被設為 nullptr 進而被刪除,可以在 lock 的保護下先複製 m_foo 到區域變數再呼叫它的 method,像是這樣:

shared_ptr<Foo> foo;
{
std::lock_guard<std::mutex> lock(m_mutex);
foo = m_foo;
}
if (foo)
foo->DoSomething();

其它許多 member field 如果複製成本不高的話,也是盡量複製到區域變數再使用,減少存取共用變數的時間,也就減少用 lock 保護的時間。

先釋放 lock 再呼叫 callback

這算是上點的特例,但很重要所以抽出來特別講。

為了避免 dead lock 還有減少 lock contention,程式的執行權要轉到其它物件前,看看能否釋放 lock。這在呼叫 callback 的時候特別重要,畢竟我們不知道上層使用者到底會作什麼,如果 callback 作了一堆事最後又回來用自己,可能會變成 acquire lock 兩次。在不允許 recursive lock 的情況下程式會掛掉。又或著這個 callback 還沒結束又觸發別的 thread 來存取同一個物件,搞不好還造成 dead lock。

下面是的寫法可以避免這些問題:

{
Callback* callback = nullptr;
{
std::lock_guard<std::mutex> lock(m_mutex);
callback = m_callback;
}
if (callback) {
SomeValue ret = callback();
// Use ret to do more things ...
}
}

在 lock 的保護下複製 m_callback 到 callback,解開 lock 再呼叫 callback。

Active Object

Wikipedia 介紹有 Java 的例子,看例子會比較好懂。

Active Object 有幾個特點:

  • class 提供的函式都是非同步操作。一呼叫就結束,若需要回傳值就在參數中帶入 callback。
  • class 本身有自己的 worker thread,所有呼叫的參數包成一個命令傳入私有的 queue 裡,worker thread 的主程式會不停的讀 queue 然後執行。通常只需要一個 worker thread。
  • 因為資料都在同一個 thread 執行,所以不用擔心 data race → 不需要用 lock → 沒有 dead lock 或少 lock 的問題。
  • 若呼叫端需要同步操作,在 class 需要同步操作函式的參數裡帶上 bool wait,預設為 false。當 wait=true 時,內部實作用 conditional variable (e.g., pthread 的 pthread_cond_t) 擋住呼叫的 thread,待 worker thread 完成工作後再通知 caller thread 返回。

我覺得這個方法非常有效地簡化 multithreading 的複雜度。唯一的問題是這個作法寫起來很囉嗦。如果有類似 Chromium MessageLoop 的類別,可以簡化一些,直接將 method call 存在 queue 裡,然後 thread 從 queue 裡讀資料再呼叫 method。我不熟 C++ 的 lambda 和 closure,應該可以像 Java 的範例那樣寫得比較簡潔吧。有需要再來研究。

懶得全部 API 都提供兩組 API,也可以將「在正確 thread 執行」的責任交給呼叫者,然後在內部全部函式都加上 thread checker,只在 debug build 時檢查是否有在不對的 thread 執行。thread checker 的實作方式很簡單,記下 object 第一次呼叫函式的 thread id,往後被呼叫時檢查是否是同個 thread id。 Chromium 裡面有大量使用這種模式。

設置 thread name

pthread_setname_np() 可以設 pthread 的名稱,這樣在用 gdb 的時候方便對照。C++11 的 thread 沒看到類型的 API,不確定各家 debugger 怎麼和它配合。比較苦工的作法是自己用 thread_local 的變數存名稱,然後在 debugger 內直接讀,就沒 pthread 那麼方便了。

寫 log 的時候順便寫 thread id 和目前時間 (單位至少 ms)

這樣讀 log 的時候方便看出多個 thread 同時讀寫造成的問題。

--

--