最近看到不錯的寫法, 備忘一下。
通常我們不希望 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 而沒有反應的話, 使用者會覺得軟體有問題。