fopen+std::thread多线程下载失败因file*非线程安全;应改用pwrite(linux/macos)或setfilepointerex+writefile(windows)实现无锁分段写入,并确保服务端支持range请求及预分配文件空间。

为什么 fopen + std::thread 直接拼多线程下载会失败
因为 C++ 标准库的 FILE*(fopen 返回)不是线程安全的,多个线程同时调用 fwrite 写同一文件句柄会导致数据错乱、覆盖或崩溃。即使加锁,也会让写入退化为串行,失去“高速”意义。真正可行的路径是:每个线程独占一个 std::ofstream(或 fd),但只写自己负责的字节段 —— 这要求提前预分配文件并支持随机偏移写入。
怎么用 pwrite 实现无锁多线程分段写入(Linux/macOS)
pwrite 是系统调用,允许指定偏移量写入,不改变文件当前 offset,天然适合多线程并发写同一文件的不同区域。但必须确保:文件已存在且大小足够,否则写入越界会静默失败或截断。
- 先用
truncate或ftruncate预分配完整大小(比如 1GB 文件,就truncate("out.bin", 1073741824)) - 计算每线程负责的字节范围:如总长
total_len、线程数n,第i个线程写[i * chunk, min((i+1) * chunk, total_len)) - 每个线程用
pwrite(fd, buf, size, offset)写,无需加锁 - 注意
fd必须是用O_RDWR | O_DIRECT(可选,绕过页缓存提升吞吐)打开的,且buf内存需对齐(posix_memalign)
Windows 下对应方案:用 SetFilePointerEx + WriteFile 模拟 pwrite
Windows 没有 pwrite,但可用 SetFilePointerEx 定位 + WriteFile 组合实现等效行为。关键点在于:每次写前都显式定位,且用 OVERLAPPED 结构体避免线程间 offset 冲突。
- 打开文件时必须带
FILE_FLAG_NO_BUFFERING(若需对齐写)和FILE_FLAG_OVERLAPPED(异步 I/O 更高效) - 每个线程分配独立
OVERLAPPED实例,并设置Offset和OffsetHigh字段为目标位置 - 调用
WriteFile时传入该OVERLAPPED*,系统自动按指定 offset 写入,互不干扰 - 不要用
SetFilePointerEx后直接WriteFile(没OVERLAPPED时仍受共享 file pointer 影响)
HTTP 分片下载的关键:服务端是否支持 Range 请求
多通道下载的前提是服务端返回 Accept-Ranges: bytes,且每个线程发带 Range: bytes=1024-2047 的请求。否则所有线程都会收到完整响应,造成重复下载和带宽浪费。
- 用
HEAD请求先检查响应头是否有Accept-Ranges,没有就降级为单线程 - 用
Content-Length头确定总大小,再均分区间;注意最后一个区间上限要设为Content-Length - 1 - CURL 示例:设置
curl_easy_setopt(curl, CURLOPT_RANGE, "1024-2047"),配合WRITEFUNCTION把数据写入对应内存 buffer - 别忽略
416 Range Not Satisfiable错误 —— 可能因服务端动态生成内容或 CDN 缓存策略导致分片失效
最易被忽略的是文件系统对稀疏文件的支持:预分配后若未实际写入,某些文件系统(如 ext4 默认)不会真正占用磁盘空间,但 NFS 或某些容器环境可能表现异常;上线前务必在目标部署环境验证 truncate + pwrite 行为一致。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











