16. 多线程网络编程:多线程TCP服务器模型、线程池+epoll模型、并发连接处理、性能调优

网络编程这块,说实话,是很多C语言开发者从「单机」迈向「分布式」的一道坎。我记得刚入行那会儿,写一个简单的echo服务器,单线程、阻塞IO,客户端一多就卡死。后来带我的老大哥丢给我一句话:「你想想看,一个服务员同时只能服务一个客人,那餐厅还怎么开?」

嗯,从那天起,我开始认真研究多线程网络编程。今天这一章,我们就来聊聊多线程TCP服务器、线程池+epoll模型,以及怎么把并发连接处理到极致。

16.1 多线程TCP服务器模型

最简单的多线程模型,就是「一个连接一个线程」。主线程负责accept,每来一个客户端,就创建一个新线程去处理。

代码大概长这样:

// 伪代码:每连接一线程模型
while (1) {
    client_fd = accept(server_fd, ...);
    if (client_fd > 0) {
        pthread_t tid;
        // 创建线程处理该客户端
        pthread_create(&tid, NULL, client_handler, &client_fd);
        pthread_detach(tid);  // 分离,不等待
    }
}

void *client_handler(void *arg) {
    int fd = *(int*)arg;
    // 处理读写...
    close(fd);
    return NULL;
}

这个模型简单直观,但问题也很明显。我在项目中遇到过一台服务器同时接入几千个客户端,线程数直接飙到几千。线程创建销毁的开销、上下文切换的代价,让CPU直接跪了。说白了,这种模型只适合连接数少、请求处理快的场景。

注意: 每连接一线程模型,当并发连接数超过几百时,性能会急剧下降。线程不是越多越好,操作系统调度是有成本的。

16.2 线程池+epoll模型

那怎么解决?线程池。说白了就是提前创建好一批线程,有任务来了就丢给空闲线程去处理,没有任务就等着。避免了频繁创建销毁的开销。

而epoll,是Linux下最高效的IO多路复用机制。它不像select/poll那样每次都要遍历所有fd,而是只返回有事件发生的fd。复杂度从O(n)降到了O(1)。

我个人的习惯是:epoll负责事件通知,线程池负责业务处理。两者结合,堪称绝配。

核心思想: epoll主线程只做一件事——监听事件。一旦有新的连接或数据到达,就把任务丢给线程池去处理。主线程绝不阻塞在业务逻辑上。

16.3 并发连接处理

并发连接处理,核心在于两点:

  • 事件分发要快——epoll_wait返回后,要能快速定位到哪个fd有事件
  • 业务处理要异步——不要在事件循环里做耗时操作

我曾经在一个项目中,把数据库查询直接放在了epoll的事件处理回调里。结果呢?一个慢查询拖垮了整个事件循环,其他所有连接都跟着遭殃。嗯,这个坑我踩过,你们就别踩了。

正确的做法是:

  1. epoll_wait 获取就绪事件
  2. 将任务封装成结构体,丢入线程池的任务队列
  3. 工作线程从队列取出任务,执行真正的读写和业务逻辑
  4. 处理完成后,如果需要继续监听,重新注册到epoll

这里我画了一张流程图,帮你理清整个逻辑:

线程池 + epoll 模型流程图 主线程 (epoll_wait) 事件就绪 (accept/read/write) 任务队列 (线程安全) 工作线程 1 工作线程 2 工作线程 3 处理完成 → 重新注册到 epoll / 关闭连接

16.4 性能调优

模型搭好了,不代表性能就上去了。调优才是真正的硬功夫。我总结了几条实战经验:

调优项 说明 建议值
线程池大小 不是越大越好,取决于CPU核心数和任务类型 CPU密集型:N+1;IO密集型:2N(N为CPU核心数)
epoll事件大小 epoll_wait一次最多返回的事件数 64~256,根据实际并发量调整
任务队列长度 防止任务堆积导致内存暴涨 建议设置上限,超过则拒绝新连接
TCP参数 减少TIME_WAIT、调整缓冲区 开启SO_REUSEADDR,调整tcp_tw_reuse
锁粒度 任务队列的锁竞争是瓶颈 使用无锁队列或分段锁
小技巧: 我个人习惯用perf工具来定位热点。先跑一轮压力测试,看看CPU花在哪里。如果锁竞争严重,考虑用CAS无锁队列;如果是epoll_wait本身耗时,检查是不是事件处理太慢。

16.5 避坑指南

最后,分享几个我踩过的坑:

  • 线程安全问题——任务队列、共享数据一定要加锁,或者用原子操作。我曾经因为忘记加锁,导致两个线程同时处理同一个连接,数据全乱了。
  • 文件描述符泄漏——每个accept得到的fd,一定要确保在某个地方被close。线程池处理完任务后,记得关闭连接。
  • 惊群问题——多个线程同时epoll_wait同一个fd,会导致惊群。解决办法是用EPOLLEXCLUSIVE标志,或者只在主线程做epoll。
  • 信号处理——多线程环境下,信号的处理要格外小心。我建议用signalfd来统一处理信号,避免信号打断epoll_wait。

嗯,这一章的内容就到这里。多线程网络编程,说白了就是「事件驱动 + 线程池」的组合拳。模型选对了,调优做到位,你的服务器就能扛住成千上万的并发连接。


公众号:蓝海资料掘金营,微信deep3321