2017-10-18 46 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