弗雷德可以不租吗?

Can fread be non rentrant?

本文关键字:弗雷德      更新时间:2023-10-16

在一个用C++编写并在Windows下用MinGW-w64编译的程序中,我在单独的线程中同时读取了几个文件。由于文件名可能包含非 ASCII 字符,因此我无法使用C++标准库std::ifstream因为它不支持wchar文件名。所以我需要将 C 库与 Win32 API 中的_wfopen一起使用。

但是,我得到了一个非常奇怪的错误,我已经在MCVE中重现了该错误。使用 fread() 读取 n 个字节后,_ftelli64的结果有时不会增加 n,而是少或多几个字节。

通过单线程读取,问题消失了,std::ifstream

消失了。它的行为就好像 fread 中存在竞争条件,然后是不可重入的。

在下面的示例中,我用fopen替换了_wfopen,因为错误仍然存在。

#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 errorn";
}
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

编辑:这似乎与_ftelli64有关

如果我用ftell替换_ftelli64,问题不再存在 那么这是_ftelli64的破碎而不是重入实现吗?

由于您主要询问的是 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 库文档)

Microsoft的"文件指针"与ISO C的"文件位置"是一回事。 总的来说,我可以看到观察到的行为符合性的唯一方法是,如果一些fread()调用遇到错误。 您可以通过在fread()返回 0 的情况下调用ferror()来检查这一点。 如果有错误,那么所有的赌注都关闭了。