C++20 同步的輸出 stream

在寫多執行序的程式的時候,如果有需要把文字輸出到 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 輸出文字的時候,不會被其他執行序打斷、插進來輸出了。

關於 mutexlock_guard 這部分的說明,則可以參考之前的《C++ 的多執行序程式開發 Thread:多執行序之間的溝通(一)》這篇文章。


而到了 C++20,STL 則是提供了「Synchronized Output Streams」的相關功能,讓開發者可以更簡單地達到輸出的同步。

他的 header 檔是 <syncstream>,相關的說明可以參考 Cpp Reference

要使用的話,一般就是透過他提供的 std::osyncstreamstd::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(flushendl)、都不會真的送出;而是會等到 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++ 了?


參考:Synchronized Output Streams with C++20

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *