2017-10-18 48 views
4

在用C++編寫的程序中,在Windows下使用MinGW-w64編譯,我在不同的線程中同時讀取多個文件。由於文件名可能有非ASCII字符,因此我不能使用C++標準庫std::ifstream,因爲它不支持wchar文件名。所以我需要使用Win32 API中的_wfopen的C庫。難道不是非租戶?

但是,我收到了一個非常奇怪的錯誤,我在MCVE中進行了複製。使用fread()讀取n個字節後,_ftelli64的結果有時不會增加n,但會減少或減少幾個字節。

隨着單個線程讀取,問題消失了,並與std::ifstream爲好。

它表現得好像fread中存在競爭條件,然後它就不可重入。

在以下示例中,我取代_wfopenfopen作爲錯誤仍然存​​在。

#include <iostream> 
#include <vector> 
#include <string> 
#include <sstream> 
#include <fstream> 
#include <thread> 

constexpr const int numThreads = 8; 
constexpr const int blockSize = 65536+8; 
constexpr const int fileBlockCount = 48; //3MB files 

void readFile(const std::string & path) 
{ 
    std::cout << "Reading file " << path << "\n"; 
    std::vector<char> buffer(blockSize); 

    FILE * f = fopen(path.c_str(), "rb"); 
    for(int i=0;i<fileBlockCount;++i) 
    { 

     int64_t pos_before = _ftelli64(f); 
     int64_t n = fread(buffer.data(), 1, buffer.size(),f); 
     int64_t pos_after = _ftelli64(f); 
     int64_t posMismatch = (int64_t)pos_after-(pos_before+n); 
     if(ferror(f)) 
     { 
      std::cout << "fread error\n"; 
     } 
     if(posMismatch!=0) 
     { 
      std::cout << "Error " << path 
        << "/ftell before " << pos_before 
        << "/fread returned " << n 
        << "/ftell after " << pos_after 
        << "/mismatch " << posMismatch << "\n"; 
     } 
    } 
    fclose(f); 
} 

int main() 
{ 
    //Generate file names 
    std::vector<std::string> fileNames(numThreads); 
    for(int i=0;i<numThreads;++i) 
    { 
     std::ostringstream oss; 
     oss << i << ".dat"; 
     fileNames[i] = oss.str(); 
    } 


    //Create dummy data files 
    for(int i=0;i<numThreads;++i) 
    { 
     std::ofstream f(fileNames[i], std::ios_base::binary); 
     for(int j=0;j<blockSize*fileBlockCount;++j) 
     { 
      f.put((char)(j&255)); 
     } 
    } 


    //Read data files in separate threads 
    std::vector<std::thread> threads; 
    for(int i=0;i<numThreads;++i) 
    { 
     threads.emplace_back(readFile, fileNames[i]); 
    } 

    //This waits for the threads to finish 
    for(int i=0;i<numThreads;++i) 
    { 
     threads[i].join(); 
    } 
    threads.clear(); 

    std::cout << "Done"; 
} 

輸出是隨機的東西,如:

Error 3.dat/ftell before 65544/fread returned 65544/ftell after 131089/mismatch 1 
Error 7.dat/ftell before 0/fread returned 65544/ftell after 65543/mismatch -1 
Error 7.dat/ftell before 65543/fread returned 65544/ftell after 131088/mismatch 1 
Error 3.dat/ftell before 2162953/fread returned 65544/ftell after 2228498/mismatch 1 
Error 7.dat/ftell before 2162952/fread returned 65544/ftell after 2228497/mismatch 1 
Error 3.dat/ftell before 3080570/fread returned 65544/ftell after 3146112/mismatch -2 
Error 7.dat/ftell before 3080569/fread returned 65544/ftell after 3146112/mismatch -1 
Error 2.dat/ftell before 65544/fread returned 65544/ftell after 131089/mismatch 1 
Error 6.dat/ftell before 0/fread returned 65544/ftell after 65543/mismatch -1 
Error 6.dat/ftell before 65543/fread returned 65544/ftell after 131088/mismatch 1 
Error 2.dat/ftell before 2162953/fread returned 65544/ftell after 2228498/mismatch 1 
Error 6.dat/ftell before 2162952/fread returned 65544/ftell after 2228497/mismatch 1 
Error 2.dat/ftell before 3080570/fread returned 65544/ftell after 3146112/mismatch -2 
Error 6.dat/ftell before 3080569/fread returned 65544/ftell after 3146112/mismatch -1 

編輯:如果我通過ftell更換_ftelli64這似乎與_ftelli64

,這個問題是不存在了 那麼這是一個破碎而不是實體_ftelli64

+0

好吧,顯然'fread()'*可以是不可重入的。我猜這個問題是它是否在Win32上是不可重入的,也許這是否符合要求。 –

+0

根據microsoft(查看https://msdn.microsoft.com/en-us/library/0ys3hc0b.aspx中的評論)從'ftell()'或(如您使用的那樣)'_ftelli64() '由最後的I/O操作決定。除非有可能出現一些不匹配現象,例如你所看到的,否則他們不會這樣說。 – Peter

+0

@Peter,這些註釋似乎主要針對以追加模式打開的文件,其中下一次寫入將始終發生在文件末尾,可能不會在最後一次讀取後立即寫入。 –

回答

3

既然你問主要是關於C標準庫,C標準說:

每個流都有一個相關的鎖,用來防止數據競爭,當執行訪問流多線程,以限制多線程執行的流操作的交錯。一次只能有一個線程持有此鎖。鎖是可重入的:單個線程可以在給定的時間多次保持鎖。

讀取,寫入,定位或查詢流的位置的所有函數都會在訪問流之前鎖定流。訪問完成後,他們釋放與流關聯的鎖。

C2011 7.21.2/7-8

C++人們應該注意到在C, 「流」 是指經由FILE *訪問之類的話。 fread(),該標準部分地說,

流(如果定義)的文件位置指示符被成功讀取的字符數提前。

fread函數返回元件的數量成功讀取

而且

如果發生錯誤,將得到的文件位置指示器的值因爲流是不確定的。

C2011, 7.21.8.1/2-3

好象沒有表徵到達流作爲錯誤的結束。

儘管C11沒有具體說明fread()必須是線程安全的,但它確實承認存在多線程程序並定義其語義。它指定在這樣的程序中,

每個線程的執行按本標準其餘部分的規定進行。

C2011, 5.1.2.4/1

在不同的數據流並行調用時不能提供的fread()未能表現爲記錄的可能性,並鎖定要求,我前面防止數據爭引和主治未定義的行爲,即使當它被並行調用時相同。

_ftelli64()是不是在ISO C標準庫函數,但在Win32文檔指定的相同條款的行爲,他們指定ftell()的行爲,這是一個標準庫函數。

檢索與stream關聯的文件指針的當前位置(如果有的話)。該位置表示爲相對於流開始的偏移量。

Microsoft C library documentation

微軟的 「文件指針」 是一回事ISO C的 「文件位置」。總體而言,那麼,我可以看到所觀察到的行爲符合的唯一方式是如果幾個fread()調用遇到錯誤。您可以在fread()返回0的情況下致電ferror()進行檢查。如果有錯誤,則所有投注均爲關閉。

+0

fread總是返回我詢問的字節數。 ferror總是返回0(沒有錯誤) – galinette

+0

那麼,@galinette,除非你的兩個線程正在從同一個文件讀取,否則你正在觀察微軟C庫中的一個錯誤。請注意,讀取同一個文件的多個線程確實可以產生你描述的結果。在一個或兩個線程的'_ftelli64()'調用之間偶爾會發生兩個不同線程對同一個文件的'fread()'調用,從而產生您觀察到的行爲。這與您觀察到的差異似乎在逐個文件基礎上平衡爲零的事實一致。 –

+0

寧可MinGW DLL的一個錯誤。其中一個MinGW開發人員告訴我_ftelli64不是由msvcrt.dll導出的,所以他們重新實現了它(並可能打破它) – galinette