Linux环境下多Socket发送数据的最优send()线程数量探讨
咱们先直接给结论:现代Linux下,最优线程数不一定等于物理核心数C,核心得看你的send()操作是偏CPU密集还是IO密集,还有内核现在的网络栈调度逻辑——和你说的90年代BSD那套单线程队列机制已经完全不一样了。
一、先搞懂现代Linux怎么处理send()调用
send()本质上是把用户态的数据拷贝到内核的socket发送缓冲区(也就是sk_buff结构体),然后交给内核网络子系统负责后续的发送调度。和你提到的老BSD比,现在的逻辑有几个关键变化:
- 多队列并行处理:现在Linux内核支持多队列网卡,还配合RPS/RFS机制,TCP/IP栈的关键路径(比如发送队列处理、软中断)都能让多个CPU核心并行干活,再也不是当年那种所有请求挤一个全局队列、单线程处理的情况了。
- 并发安全的socket操作:每个socket的发送操作本身是线程安全的,但内核用了per-CPU队列或者轻量锁来减少竞争。当多个线程同时给不同的socket调用
send()时,内核可以并行处理这些数据拷贝和队列提交操作,不会被单一线程卡住瓶颈。 - 数据拷贝的并行性:
send()里最耗时的环节之一是用户态到内核态的数据拷贝,现代CPU可以用DMA或者零拷贝技术(比如sendfile()、splice())优化,但就算是普通send(),拷贝操作也能在多个CPU核心上并行执行——每个线程的send()对应不同socket,各自的拷贝可以同时跑在不同核心上。
二、那N个Socket发送时,到底开多少线程合适?
得分两种核心场景来看:
1. 发送前的用户态处理很轻量
如果你的线程除了调用send(),几乎没什么CPU密集型操作(比如只是把提前准备好的数据传给send()):
- 开和物理核心数C相当的线程,每个线程负责一部分socket的发送,性能会比单线程串行所有
send()好太多——因为多个核心可以并行处理数据拷贝和内核提交,完全利用硬件资源。 - 但线程数别超过物理核心太多,不然上下文切换的开销会盖过并行收益,反而拖慢速度。
2. 发送前的用户态处理很繁重
如果每个send()之前要做大量CPU密集型计算(比如加密、压缩数据),那线程数可以适当超过物理核心数(比如2*C)——因为这时候线程大部分时间在做用户态计算,send()的IO等待时间可以被其他线程利用。不过这种情况更推荐用异步IO(比如epoll+非阻塞socket)替代多线程,效率会更高。
还有个特殊情况:如果你的网卡是老掉牙的单队列网卡,那内核的发送软中断可能只能在一个核心上跑,这时候就算你开多线程send(),最终硬件发送还是串行的,多线程的收益会打折扣,但用户态到内核态的拷贝还是能并行的,比单线程还是强。
三、对比你说的90年代BSD情况
你提到的90年代BSD单线程队列处理,是因为当时网络栈设计简单,所有socket的发送请求都挤到一个全局队列,由单一线程(或者软中断)处理,所以多线程send()和单线程串行send()的性能差不了多少,主要差在用户态的线程调度开销。
但现代Linux早就抛弃了这种设计:
- 每个socket有自己的发送队列(
sk->sk_write_queue),内核用per-CPU的软中断处理发送,多个核心可以同时处理不同socket的发送请求。 - 网卡多队列技术让硬件层面也能并行发送不同socket的数据,进一步拉满并发能力。
四、你给出的两种代码模式性能对比
你提到的两种代码:
模式1:多线程分别给不同socket发数据
thread1 -> send(sock1, ..) thread2 -> send(sock2, ..) ... threadN -> send(sockN, ..)
模式2:单线程串行给所有socket发数据
thread -> send(sock1, ..), send(sock2, ..), ..., send(sockN, ..)
在现代Linux下,模式1的性能会显著优于模式2,尤其是当N很大的时候——模式1能利用多个CPU核心并行处理数据拷贝和内核提交,而模式2只能串行处理,就算内核后台能并行发送,用户态的send()调用本身是串行的,会成为瓶颈。
不过如果每个send()发送的数据量极小(比如只有几个字节),那模式1的线程调度开销可能会抵消并行收益,这时候单线程或者用批量发送(比如writev())可能更划算。
内容的提问来源于stack exchange,提问作者Darkkey

