multiple thread 存取共享資料的小技巧

最近看到不錯的寫法, 備忘一下。

通常我們不希望 main thread 被 block [*1], 所以會另外開 worker thread 處理耗 CPU 的工作。主要的需求是避免 block main thread, 其次是縮短完成工作的時間。

由於 main thread 和 worker thread 需要互相傳遞一些資料, 有可能造成 race condition, 導致程式行為有時不正確或是掛掉。這裡討論幾種避免 race condition 的作法。

用一個 "global" lock 保護整個 thread 的資料

worker thread 全部函式都用同一個 lock 保護。

  • 優點: 容易實作。
  • 缺點: main thread 在 worker thread 忙的時候存取共享資料, 還是會被 worker thread block。

不同群共享的資料用不同的 lock 保護

和上個作法類似, 不過資料分群用不同 lock 保護。

  • 優點: 減少 main thread 被 block 的機會。
  • 缺點: 實作有些複雜, 可能會漏保護到新加的變數。並且 main thread 仍有可能被 block。

避免共享資料

如果資料只有一個 thread 會用到, 就直接轉交給另一個 thread (改變 ownership)。若兩個 thread 都會用到, 先複製一份再轉交給另一個 thread。沒有共享的資料, 就不需用 lock 保護了 (但仍需透過 message loop 或 queue 傳遞資料到另一個 thread)。

  • 優點: 複製的時間不長, 且可以掌控占據 main thread 的時間, 完全不受 worker thread 影響
  • 缺點: 需要仔細設計複製和傳遞資料的機製, 確保沒有共享資料。

需要同步資料的時候, 記得只能允許一個方向的 block。比方說 main thread 可以透過 conditional variable 來 block worker thread, 但不允許反過來的操作, 這樣就不會有 dead lock。

不用 lock 的注意事項

每個函式都要放 assert 確保函式在正確的 thread 執行。若一個 class 有兩份資料分別在兩個 thread 下執行的話, 可以考慮訂兩個 struct, 比方說 MainData 和 WorkerData, 然後透過 private method 存取資料, 藉此確保 thread safe:

// C++
class MyClass
{
public:
   ...

private:
  MainData& GetMainData();
  WorkerData& GetWorkerData();

  struct MainData {
    ...
  };
  struct WorkerData {
    ...
  };
  MainData m_mainData;
  WorkerData m_workerData;
};

MainData& MyClass::GetMainData()
{
  assert(InMainThread());
  return m_mainData;
}

WorkerData& MyClass::GetWorkerData()
{
  assert(InWorkerThread());
  return m_workerData;
}

這樣程式寫錯時會造成 assert failed, 可以很快地修正。

備註

*1 比方說 daemon 的 main thread 會不斷收新的連線, GUI 的 main thread 會收使用者的輸入。main thread 被 block 而沒有反應的話, 使用者會覺得軟體有問題。

留言

這個網誌中的熱門文章

(C/C++ ) 如何在 Linux 上使用自行編譯的第三方函式庫

熟悉系統工具好處多多

virtualbox 使用 USB 裝置