多进程并发场景下std::filesystem目录遍历不稳定问题排查
跨平台glob实现的并发稳定性问题及解决办法
问题现象
用std::filesystem递归遍历目录结合正则匹配实现跨平台glob功能时,单进程运行完全正常,但在同一机器后台并发运行100个该可执行文件进程(带不同参数)时,大量进程抛出如下错误并崩溃:
terminate called after throwing an instance of 'std::filesystem::__cxx11::filesystem_error' what(): filesystem error: cannot increment recursive directory iterator: No such file or directory (core dumped)
改用boost::filesystem实现会出现同样问题,但依赖Unix原生glob.hpp库的glob方法运行稳定。
稳定的Unix原生glob实现代码
std::vector<std::string> glob(const std::string& pattern) { glob_t g; glob(pattern.c_str(), GLOB_TILDE, nullptr, &g); // 需确保glob返回0 std::vector<std::string> filelist; filelist.reserve(g.gl_pathc); for (size_t i = 0; i < g.gl_pathc; ++i) { filelist.emplace_back(g.gl_pathv[i]); } globfree(&g); return filelist; }
问题复现代码及步骤
最小可复现代码
#include <regex> #include <filesystem> #include <glob.h> #include <iostream> namespace fs = std::filesystem; std::vector<std::string> glob(const std::string& regexPattern) { std::vector<std::string> matches; fs::path currentPath = fs::current_path(); fs::path dirPath = regexPattern; if (dirPath.is_relative()) { dirPath = currentPath / dirPath; } size_t pos = 0; std::string path_upto_wildcard = "\"/\""; std::string str_dirPath = dirPath.string(); while ((pos = str_dirPath.find("/")) != std::string::npos) { std::string token =str_dirPath.substr(0, pos); str_dirPath.erase(0, pos + 1); if (token == "" ) continue; if (token.find("*") != std::string::npos) continue; path_upto_wildcard += token + "/"; } dirPath = path_upto_wildcard; std::regex regEx(regexPattern); fs::recursive_directory_iterator endIterator; for (fs::recursive_directory_iterator it(dirPath); it != endIterator; ++it) { if ( std::regex_match(it->path().string(), regEx)) { matches.push_back(it->path().string()); } } return matches; } int main() { std::vector dirs{"\"/mypath/a/.*XYZ*/.*\"", "\"/mypath/b/.*XYZ*/.*\"", "\"/mypath/c/.*XYZ*/.*\"", }; for (auto& dir : dirs) { std::vector<std::string> matches = glob(dir); for (auto& match : matches) { std::cout << match << std::endl; } } return 0; }
编译命令
g++ test_globbing.cxx -o test_globbing -std=c++17
复现方式
- 创建包含100行
"./test_globbing"的test_glob.txt文件 - 运行以下shell脚本:
#!/bin/bash n=0 while read arg; do echo $n && ((n+=1)); eval ' $arg &> job_$(echo $n).log &'; sleep .2; done < $1
该问题并非每次必现,但进程并发量越高,出现概率越大。
问题原因
- 文件系统竞态条件:并发遍历目录时,某进程刚获取到目录条目后,该条目(文件/子目录)可能被其他进程或系统操作删除/移动,导致迭代器递增时找不到目标,触发异常。
- 迭代器默认行为差异:
std::filesystem::recursive_directory_iterator默认遇到无法访问的条目时直接抛出异常;而Unix原生glob库内部会处理这类临时不存在的条目,不会崩溃。 - 路径处理错误:复现代码中
path_upto_wildcard初始值和dirs中的路径包含多余引号,会导致路径解析异常,并发场景下这种错误的影响被放大。
解决办法
1. 捕获迭代异常,忽略临时无效条目
修改迭代逻辑,添加异常捕获,跳过因文件临时不存在导致的错误:
// 构造迭代器时跳过权限不足的条目 fs::recursive_directory_iterator it( dirPath, fs::directory_options::skip_permission_denied ); while (it != endIterator) { try { if (std::regex_match(it->path().string(), regEx)) { matches.push_back(it->path().string()); } ++it; } catch (const fs::filesystem_error& e) { // 忽略临时不存在的文件/目录,继续迭代 ++it; } }
2. 修复路径处理的引号问题
去掉代码中多余的引号,确保路径解析正确:
// 修改path_upto_wildcard初始值 std::string path_upto_wildcard = "/"; // main函数中去掉路径的引号 std::vector dirs{"/mypath/a/.*XYZ*/.*", "/mypath/b/.*XYZ*/.*", "/mypath/c/.*XYZ*/.*", };
3. 优先使用平台原生实现(推荐)
如果需要跨平台,可做平台分支处理:Unix系统用原生glob,Windows系统用FindFirstFile系列API封装;或使用成熟的跨平台库(如Boost.Glob)。原生实现经过系统级优化,能更好处理并发场景下的文件系统竞态。
内容的提问来源于stack exchange,提问作者Mohamed Ayman Aly
相关产品推荐
相关产品推荐

