多线程上传不可在单个TCP连接上并发调用send(),因TCP流有序而send()非原子,会导致数据覆盖或错序;正确方式为分块并行上传(服务端支持)或多连接并发上传(如多socket各传一分片),需独立socket、同步元信息、线程安全进度与错误管理。

多线程上传 ≠ 开多个 send() 就完事
直接在单个 TCP 连接上开多个线程并发调用 send() 是错的——TCP 流是有序字节流,send() 不保证原子性,线程间写入会互相覆盖或打乱顺序,导致文件损坏。真正可行的加速路径只有两条:**分块并行上传(需服务端支持)** 或 **多连接并发上传(如 FTP 的 PORT/PASV 多通道、HTTP/2 多路复用)**。C++ 里最常用且可控的是后者。
用 std::thread 管理多个 TCP 连接上传
核心思路是:每个线程独占一个 socket,各自连接服务器、发送自己负责的文件分片。注意不是“把一个文件切开让多个线程往同一个 socket 写”,而是“多个 socket 同时传多个分片”。服务端必须能识别分片序号并重组。
- 客户端需预读文件大小,按固定块大小(如
64 * 1024)计算分片数 - 每个线程打开独立
socket,调用connect()连接同一服务器不同端口(或复用同一端口但服务端支持多连接) - 线程内用
fseek()+fread()定位到本分片起始偏移,读取后调用send()发送 - 必须同步发送元信息(文件名、总大小、分片索引、偏移量),否则服务端无法拼接
- 所有线程结束后,需等待服务端返回合并完成确认,不能仅靠本地线程结束就认为上传成功
std::async 和线程池比裸 std::thread 更稳
频繁创建销毁 std::thread 开销大,且异常未捕获会导致程序终止。用 std::async 配合 std::launch::async 能自动管理生命周期;更推荐封装成简易线程池,限制并发数(比如最多 4 个上传线程),避免耗尽 socket 资源或触发服务器限流。
- Windows 下默认进程最大 socket 数约 16384,但实际受系统资源和服务器策略限制
- Linux 可通过
ulimit -n查看,上传线程数建议 ≤ 8,尤其在嵌入式或容器环境 - 每个线程必须有自己的
std::ifstream实例,共享文件句柄会导致fseek()冲突 - 别忘了设置 socket 选项:
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag))关闭 Nagle 算法,减少小包延迟
最容易被忽略的坑:进度同步与错误传播
多线程上传时,主线程要实时知道整体进度和任一失败。用 std::atomic_int 记录已上传字节数不够——它不保证跨线程内存可见顺序,且无法携带错误码。正确做法是:
立即学习“C++免费学习笔记(深入)”;
- 每个线程结束时向线程安全队列(如
boost::lockfree::spsc_queue或带互斥锁的std::queue)推送{chunk_id, bytes_sent, error_code} - 主线程轮询该队列,聚合进度、检查是否所有分片成功、收集首个错误用于快速失败
- 任意线程遇到
send()返回 -1 或超时,应立即通知其他线程取消(用std::atomic_bool共享取消标志),避免继续无效发送 - 别依赖
std::thread::joinable()判断是否完成——线程可能已崩溃退出但未被 join,要用明确的状态机管理
分片上传的可靠性不在于“快”,而在于“可中断、可重试、可验证”。哪怕只慢 10%,只要每个分片有校验和、失败后能单独重传,整体成功率就远高于单连接暴力上传。


















