多线程下载应避免fseek+fwrite,改用pwrite(Linux/macOS)或SetFilePointerEx+WriteFile(Windows)实现原子写入,并预分配文件大小;仅对预分配和元数据更新加锁,分块大小需动态调整,Windows创建文件句柄须显式设置FILE_SHARE_READ。

多线程下载时 fseek + fwrite 直接写入会覆盖或错位
分块下载后直接用普通文件指针写入,几乎必然出问题:多个线程同时 fseek 到不同偏移再 fwrite,底层文件指针(FILE* 的内部状态)不是线程安全的,实际写入位置不可控,轻则数据错乱,重则部分块被截断。
正确做法是绕过 C 标准库的缓冲和指针管理,改用系统调用级原子写入:
- Linux/macOS 用
pwrite:传入绝对偏移,不依赖当前文件位置,天然线程安全 - Windows 用
SetFilePointerEx+WriteFile,但必须确保每次写前重置指针且加FILE_FLAG_NO_BUFFERING(否则缓存干扰) - 所有平台都推荐先创建空文件并
ftruncate/SetEndOfFile预分配大小,避免写入时扩容导致的竞态
std::shared_mutex 不适合保护整个文件写入过程
有人想用读写锁串行化所有写操作,这反而把并发下载变成串行写入,吞吐量暴跌——网络等待时间被白白浪费,CPU 和磁盘也闲着。
真正要锁的,只是「预分配文件大小」和「更新已写块元数据」这两处极短临界区:
立即学习“C++免费学习笔记(深入)”;
- 预分配只需在所有线程启动前做一次,用
std::call_once+std::once_flag足够 - 记录哪些块已写完(比如用
std::vector<:atomic>></:atomic>),根本不需要锁——store是原子的 - 只有合并阶段(如校验、重排)才需协调,那时下载早已结束,不属并发范畴
分块大小设成 1MB 比 64KB 更稳,但别硬编码
分块太小(如 8KB)会导致 HTTP 请求头开销占比飙升、线程调度压力大;太大(如 50MB)则单块失败重试成本高,内存占用陡增,且可能触发某些服务器的 range 请求限制。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
经验值和适配逻辑比固定值更重要:
- 起始块大小建议 1–2MB,根据响应头
Content-Range中的总长度动态调整:总长 1GB → 4MB - 务必检查服务器是否支持
Range:发 HEAD 请求,看响应头有无Accept-Ranges: bytes - 遇到 416 错误时,不是立刻重试,而是回退到从 offset 0 开始整文件下载(有些 CDN 对 range 边界敏感)
Windows 下 CreateFile 必须带 FILE_SHARE_READ
即使你只写不读,其他线程或后续校验工具(如 md5sum)可能需要同时读该文件。Windows 默认禁止共享读,会导致 ERROR_SHARING_VIOLATION,而 Linux 的 open 默认允许。
所以 Windows 创建句柄时必须显式声明:
HANDLE h = CreateFileA(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
漏掉 FILE_SHARE_READ,哪怕所有线程都只写,只要有一个外部进程尝试打开读,就会阻塞或失败——这点容易被忽略,尤其在调试时用资源管理器查看文件就触发了。
跨平台封装时,别迷信 std::ofstream,它在 Windows 下默认不共享,且无法指定底层标志,必须直调系统 API 或用 boost::iostreams 这类可控底座。


















