在寫多執行序的程式的時候,如果有需要把文字輸出到 console 的時候,應該都有機會碰到不同執行序的文字交錯出現、導致難以閱讀的問題。
像下面就是一個簡單的例子:
#include <iostream> #include <thread> #include <vector> void test(const int idx) { for (int i = 0; i < 3; ++i) std::cout << "Thread " << idx << " step " << i << std::endl; } int main() { std::vector<std::thread> poolThread; for (int i = 0; i < 3; ++i) poolThread.emplace_back([i]() { test(i); }); for (auto& rThread : poolThread) rThread.join(); }
在上面的例子裡面,總共建立了三個 thread、各自去執行 test() 這個函式;而 test() 裡面,則是簡單地透過 std::cout 輸出三次訊息。
理論上,這樣應該會交錯出現不同 thread、一行一行的訊息;但是實際上,看到的畫面很有可能會像下面這樣:
Thread Thread Thread 1 step 0 Thread 1 step 1 Thread 1 step 2 2 step 0 Thread 2 step 1 Thread 2 step 2 0 step 0 Thread 0 step 1 Thread 0 step 2
可以看到,某幾行的文字已經整個混在一起、變成難以閱讀了。
實際上,C++11 的標準雖然保證了 std::cout 這類的全域 stream 物件都是 thread-safe 的,但是還是會有 race condition 的狀況產生。
而如果要避免這樣的問題發生,比較標準的方法,就是透過 mutex 來做同步了;下面就是一個簡單的修改版本:
#include <iostream> #include <mutex> #include <thread> #include <vector> std::mutex coutMutex; void test(const int idx) { for (int i = 0; i < 3; ++i) { std::lock_guard<std::mutex> coutLock(coutMutex); std::cout << "Thread " << idx << " step " << i << std::endl; } } int main() { std::vector<std::thread> poolThread; for (int i = 0; i < 3; ++i) poolThread.emplace_back([i]() { test(i); }); for (auto& rThread : poolThread) rThread.join(); }
透過這樣的修改,就可以確保每次在透過 std::cout 輸出文字的時候,不會被其他執行序打斷、插進來輸出了。
關於 mutex 和 lock_guard 這部分的說明,則可以參考之前的《C++ 的多執行序程式開發 Thread:多執行序之間的溝通(一)》這篇文章。
而到了 C++20,STL 則是提供了「Synchronized Output Streams」的相關功能,讓開發者可以更簡單地達到輸出的同步。
他的 header 檔是 <syncstream>,相關的說明可以參考 Cpp Reference。
要使用的話,一般就是透過他提供的 std::osyncstream 或 std::wosyncstream 來把本來的 ostream 包一層、處理同步的操作。
要透過 std::osyncstream 來修改上面的程式的話,基本上可以寫成:
#include <iostream> #include <syncstream> #include <thread> #include <vector> void test(const int idx) { for (int i = 0; i < 3; ++i) std::osyncstream(std::cout) << "Thread " << idx << " step " << i << std::endl; } int main() { std::vector<std::thread> poolThread; for (int i = 0; i < 3; ++i) poolThread.emplace_back([i]() { test(i); }); for (auto& rThread : poolThread) rThread.join(); }
這樣的寫法會比自己去操作 mutex 方便(畢竟還得要有個全域變數在那…)、也可以確保每次輸出的過程不會被打斷,算是相對方便的~
而 std::osyncstream 實際運作的模式,可以參考 Cpp Reference 的說明:
{ std::osyncstream synced_out(std::cout); // synchronized wrapper for std::cout synced_out << "Hello, "; synced_out << "World!"; synced_out << std::endl; // flush is noted, but not yet performed synced_out << "and more!\n"; } // characters are transferred and std::cout is flushed
在把 std::cout 透過 std::osyncstream 包裝成 synced_out 後,透過他輸出的內容、就算有強制清空 buffer(flush、endl)、都不會真的送出;而是會等到 synced_out 這個物件消失後、在一口氣送出。
他應該也是透過這樣的機制,來確保操作中間被打斷的。
所以,如果把 test() 這個函式改成下面的樣子:
void test(const int idx) { std::osyncstream sos(std::cout); for (int i = 0; i < 3; ++i) sos << "Thread " << idx << " step " << i << std::endl; }
那就會發現,同一個 thread 的輸出都會在一起、而不會有不同 thread 的輸出混雜在一起的狀況。
透過這樣的方式,也可以進一步控制那些輸出要綁在一起。
不過個人覺得比較討厭的一點,是雖然 Visual Studio 目前已經支援了;但是 Ubuntu 20 預設提供的 g++ 最新版本的 10.x 還不支援、要另外安裝 g++11 才有支援…
包括 module 等功能也是,感覺現在 C++ 標準支援相對完整的,好像變成是 VC++ 了?