C/C++ 標準輸入輸出的坑
最近公司專案需要分析日誌,我拿到的日誌經過了一次處理,以Json格式儲存,日誌量每小時大約1G,行數大約60萬,此為背景。
其實對於這類問題,通常的解法是寫個指令碼去跑。對於我來說,主業是C/C++,指令碼就只會bash和awk,可是這兩種都無法直接處理Json;其他像python和perl可以處理但又不想學。怎麼辦呢?我想到的辦法是用C++設計一個小工具,它從標準輸入stdin中獲取Json資料,然後取出我們感興趣的欄位,最後從stdout輸出。這樣就可以在bash中利用管道將不同的處理流程串起來。
我首先實現了一個單執行緒版本,首先使用std::getline從std::cin中獲取一行資料,然後解析資料並提取欄位,最後通過
後來在和同事聊天時談到這個問題,同事說可以試試多執行緒。我一想,確實是這樣,我們的系統環境擁有12核CPU,32G記憶體,不好好利用實在太可惜了。於是我使用作業系統中經典的生產者/消費者模型又實現了一版。首先有1個讀取執行緒,負責從std::cin中獲取資料,並將得到的資料投遞到讀取佇列中;接著有10個解析執行緒,負責從讀取佇列中取出資料,然後解析提取欄位,並將結果投遞到輸出佇列中;最後還有1個輸出執行緒,負責從輸出佇列取出結果並使用std::cout輸出。
就在我期待奇蹟發生的時候,奇蹟果然發生了,使用這個版本處理一次大約需要4
在網上查閱部分資料後才知道,C++為了和C語言做相容,使用std::cin和std::cout做輸入輸出時會和stdin和stdout做同步,這將消耗大量時間,而這個功能可以通過std::ios::sync_with_stdio(false)關閉,條件是不能混用C/C++的輸入輸出了。試了一下,情況果然好了很多,單執行緒版本只要
就在我準備收工的時候,又發現了新的問題:輸出執行緒的結果不正確。正常情況下我們的工具遇到一條輸入資料就會產生一條輸出資料,但實際情況是輸入60萬條資料,對應的輸出有時多於60萬,有時少於60萬。通過對結果仔細分析,發現出現了資料損壞,而這在邏輯上是不可能的。
為什麼說不可能,因為我們的程式已經考慮到了多個執行緒使用std::cout輸出可能會造成資料損壞的問題,從而專門使用一個執行緒進行輸出。搜尋整個程式碼,也確實只在輸出執行緒中使用了一次std::cout,真是太奇怪了。
追查了許久,終於發現了一個奇怪的函式std::cin.tie。帶引數呼叫時用於給std::cin繫結一個輸出流,不帶引數時直接返回當前繫結的物件,而std::cin預設繫結的就是std::cout,std::cin在每次讀取之前會先對繫結的輸出流物件執行flush操作。問題終於找到了,我們雖然確保了std::cout只在一個執行緒中使用,但是C++的預設實現使讀取執行緒中std::cout也被使用到了,解決辦法就是往std::cin.tie中傳入NULL引數,解除繫結。
下面附上一段測試程式碼,用於復現這個問題。
#include <time.h>
#include <iostream>
#include <string>
#include <thread>
#include <atomic>
std::atomic<bool> running(true);
static const char * pMsg[10] = {
"This is a simple test.\n",
"This is a second test.\n",
"This is a third test.\n",
"This is a forth test.\n",
"This is a fifth test.\n",
"This is a sixth test.\n",
"This is a seventh test.\n",
"This is a eighth test.\n",
"This is a ninth test.\n",
"This is a tenth test.\n",
};
static void writing(void)
{
srand((unsigned int)time(NULL));
for(int i = 0; i < 100000; ++i)
{
std::cout << pMsg[rand() % 10];
}
running = false;
}
static void reading(void)
{
while(running && !std::cin.eof())
{
std::string line;
std::getline(std::cin, line);
}
}
int main(int argc, char ** argv)
{
std::ios::sync_with_stdio(false);
std::cin.tie(NULL);
std::thread writing_thread(writing);
std::thread reading_thread(reading);
writing_thread.join();
reading_thread.join();
return 0;
}
程式開了兩個執行緒,輸出執行緒會輸出10萬行資料,讀取執行緒只是隨便讀著玩的,使用的時候需要重定向一下輸入輸出,輸入使用一個稍大一點的文字檔案即可。通過註釋掉main函式前兩行可以試不同條件下的執行情況,可以發現一個有趣的現象,在Windows上基本上是沒有差別的,也不會出錯;而Linux上差別就大了,呵,自己慢慢體會。
上述程式碼在Windows上使用VS2013編譯通過。Linux上使用GCC 4.9.2編譯通過,編譯命令:g++ -std=gnu++11 -pthread -O3 test.cpp -o test。