在 Windows 上使用多线程文件 IO 的 SHARING_VIOLATION

问题描述

我有一些类似于这个最小复制示例的代码(真实版本生成一些代码并编译它):

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

void write(unsigned int thread)
{
    std::ofstream stream("test_" + std::to_string(thread) + ".txt");
    stream << "test" << std::endl;
    stream << "thread" << std::endl;
    stream << "bad" << std::endl;
}

void test(unsigned int thread)
{
    write(thread);
#ifdef _WIN32
    const std::string command = "rename test_" + std::to_string(thread) + ".txt test_renamed_" + std::to_string(thread) + ".txt";
#else
    const std::string command = "mv test_" + std::to_string(thread) + ".txt test_renamed_" + std::to_string(thread) + ".txt";
#endif
    system(command.c_str());
}

int main()
{
    std::vector<std::thread> threads;
    for(unsigned int i = 0; i < 5; i++) {
        // Remove renamed file
        std::remove(("test_renamed_" + std::to_string(i) + ".txt").c_str());

        threads.emplace_back(test,i);
    }

    // Join all threads
    for(auto &t : threads) {
        t.join();
    }
    return EXIT_SUCCESS;
}

我的理解是 std::ofstream 应该以良好的 RAII 方式运行,并在写入函数结束时关闭和刷新。在 Linux 上,它似乎就是这样做的。但是,在 Windows 10 上,我偶尔会收到“该进程无法访问该文件,因为它正被另一个进程使用”错误。我已经用 procmon 深入研究了它,看起来文件没有被父进程 (22224) 关闭,导致 SHARING_VIOLATION 可能导致错误

Procmon trace

尽管 procmon 跟踪看起来问题出在我的进程中,但我已尝试关闭病毒扫描程序。我还尝试使用 C 风格的 fopen、fprintf、fclose 并确保我使用 system 生成的进程不会通过清除底层文件句柄上的 HANDLE_FLAG_INHERIT 以某种方式继承文件句柄......这让我有点出乎意料!有什么想法吗?

解决方法

至少在 VS 2017 上,我可以确认文件已从您的代码段中关闭。 (在ofstream的析构函数中,代码调用了句柄上的fclose)

不过,我认为问题不在于 C++ 代码,而在于操作系统的行为。

在 Windows 中,删除操作系统认为已打开的文件的行为将被阻止。在 Unix 中,从目录中取消链接文件的行为是允许现有句柄继续作用于孤立文件。因此,在 unix 中,该操作永远不会违反共享,因为取消链接文件的行为是不同的操作。可以为最近的 Windows 10 版本选择 linux 语义。

Windows 上的 procmon 具有给定的高度。这意味着任何由病毒扫描程序执行的操作都可能对 procmon 隐藏,并且会给出错误的答案。

进程也可以在打开的文件上复制句柄,这也会导致此问题,但不会显示句柄已关闭。

,

我们可以使用Win32 API重写文件写入:

void writeRaw(unsigned int thread)
{
    const auto str = "test_" + std::to_string(thread) + ".txt";
    auto hFile = CreateFileA(str.c_str(),GENERIC_WRITE,FILE_SHARE_WRITE,nullptr,CREATE_ALWAYS,nullptr);
    DWORD ret{};
    WriteFile(hFile,str.data(),str.size(),&ret,nullptr);
    CloseHandle(hFile);
}

由于 Windows 的工作方式,运行测试仍然会导致文件共享冲突。当最后一个句柄关闭时,文件系统驱动程序执行 IRP_MJ_CLEANUP IOCTL 以完成与文件相关的任何处理。 例如,防病毒软件会尝试扫描文件(并顺便锁定它=))。此外,文档 MSDN IRP_MJ_CLEANUP 指出:

需要注意的是,当一个文件对象的所有句柄都已关闭时,这并不一定意味着不再使用该文件对象。系统组件,例如缓存管理器和内存管理器,可能持有对文件对象的未完成引用。即使在收到 IRP_MJ_CLEANUP 请求之后,这些组件仍然可以读取或写入文件。

结论:由于底层系统组件仍在处理文件关闭请求,因此如果进程在关闭句柄后不久尝试对文件执行某些操作,则预计会在 Windows 中收到文件共享冲突。

,

该问题最可能的原因是当您在 Windows 中删除文件时,它不会立即被删除(它只是被标记为删除)。它可以/将需要几毫秒(如果你很不走运,最多几秒钟)才能被真正删除。

来源:Niall Douglas 在“使用 Boost.AFIO 在文件系统上实现更好的互斥”中,大约 10 分钟:10 秒 https://www.youtube.com/watch?v=9l28ax3Zq0w