1. 程式人生 > >C/C++ 標準輸入輸出的坑

C/C++ 標準輸入輸出的坑



最近公司專案需要分析日誌,我拿到的日誌經過了一次處理,以Json格式儲存,日誌量每小時大約1G,行數大約60萬,此為背景。

其實對於這類問題,通常的解法是寫個指令碼去跑。對於我來說,主業是C/C++,指令碼就只會bashawk,可是這兩種都無法直接處理Json;其他像pythonperl可以處理但又不想學。怎麼辦呢?我想到的辦法是用C++設計一個小工具,它從標準輸入stdin中獲取Json資料,然後取出我們感興趣的欄位,最後從stdout輸出。這樣就可以在bash中利用管道將不同的處理流程串起來。

我首先實現了一個單執行緒版本,首先使用std::getlinestd::cin中獲取一行資料,然後解析資料並提取欄位,最後通過

std::cout輸出。使用這個版本處理一次要花大約5分鐘,當時覺得好像也可以接受了。

後來在和同事聊天時談到這個問題,同事說可以試試多執行緒。我一想,確實是這樣,我們的系統環境擁有12CPU32G記憶體,不好好利用實在太可惜了。於是我使用作業系統中經典的生產者/消費者模型又實現了一版。首先有1個讀取執行緒,負責從std::cin中獲取資料,並將得到的資料投遞到讀取佇列中;接著有10個解析執行緒,負責從讀取佇列中取出資料,然後解析提取欄位,並將結果投遞到輸出佇列中;最後還有1個輸出執行緒,負責從輸出佇列取出結果並使用std::cout輸出。

就在我期待奇蹟發生的時候,奇蹟果然發生了,使用這個版本處理一次大約需要4

分鐘,這顯然是不可接受的。拿著同樣的程式碼在Windows上執行(i7 48執行緒,16G記憶體),結果只需要不到30秒,後來拿前面的單執行緒版本跑也只需要大約100秒。那麼問題出在哪裡呢?通過callgrind分析後發現,程式卡在讀取執行緒中,具體是std::getline這個函式。

在網上查閱部分資料後才知道,C++為了和C語言做相容,使用std::cinstd::cout做輸入輸出時會和stdinstdout做同步,這將消耗大量時間,而這個功能可以通過std::ios::sync_with_stdio(false)關閉,條件是不能混用C/C++的輸入輸出了。試了一下,情況果然好了很多,單執行緒版本只要

94秒的樣子,效能比Windows上略好;多執行緒版本需要43秒左右,效能比Windows略差。即使如此,也非常可以接受了。

就在我準備收工的時候,又發現了新的問題:輸出執行緒的結果不正確。正常情況下我們的工具遇到一條輸入資料就會產生一條輸出資料,但實際情況是輸入60萬條資料,對應的輸出有時多於60萬,有時少於60萬。通過對結果仔細分析,發現出現了資料損壞,而這在邏輯上是不可能的。

為什麼說不可能,因為我們的程式已經考慮到了多個執行緒使用std::cout輸出可能會造成資料損壞的問題,從而專門使用一個執行緒進行輸出。搜尋整個程式碼,也確實只在輸出執行緒中使用了一次std::cout,真是太奇怪了。

追查了許久,終於發現了一個奇怪的函式std::cin.tie。帶引數呼叫時用於給std::cin繫結一個輸出流,不帶引數時直接返回當前繫結的物件,而std::cin預設繫結的就是std::coutstd::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。