Linux33 网络编程-多线程TCP并发
多线程并发是网络编程中处理多客户端连接的核心方案,核心思想是主线程负责监听连接,子线程为每个客户端提供专属服务—— 通过线程的独立性隔离客户端,同时利用 CPU 多核提升处理效率,适合中低并发(几百~几千连接)、业务逻辑复杂的场景(如 SSH、文件传输、数据库查询)。以下从原理、实现、问题、优化展开,结合实战代码深度解析:
核心原理:多线程并发的工作模型
多线程并发基于 “分工协作”,主线程与子线程各司其职,通过内核调度实现并发执行:
1. 核心角色与流程
| 线程类型 | 核心职责 | 关键操作 |
|---|---|---|
| 主线程(监听线程) | 接收客户端连接,分配任务 | socket()→bind()→listen()→accept()→创建子线程 |
| 子线程(工作线程) | 与单个客户端全量交互 | 接收数据( |
2. 并发实现的核心逻辑
主线程启动后,初始化监听套接字并绑定端口,进入循环等待客户端连接;
每收到一个新连接(
accept()返回客户端套接字client_fd),创建一个子线程;子线程接收
client_fd,与对应客户端单独交互(收发数据、业务处理),不影响其他线程;客户端断开连接后,子线程释放资源(关闭
client_fd、退出线程);主线程持续监听新连接,实现 “同时处理多个客户端” 的效果。
3. 核心优势
实现简单:无需复杂的 I/O 多路复用逻辑,通过线程天然隔离客户端;
并发高效:多线程可被调度到不同 CPU 核心,充分利用多核资源;
业务隔离:一个客户端的阻塞(如
recv等待数据、数据库查询)不会影响其他客户端;开发成本低:线程间共享进程内存(如全局配置),通信比多进程简单。
4.服务器端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>void* fun(void* arg)
{int *p = (int*)arg;int c = *p;free(p);while (1){char buff[128] = {0};int n = recv(c, buff, 127, 0); // read()if( n <= 0 ){break;// n==0,代表客户端关闭了连接, n == -1 失败}printf("buff=%s\n", buff);send(c, "ok", 2, 0); // write()}printf("client close\n");close(c);
}
int main()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP套接字,文件描述符if (sockfd == -1){exit(1);}struct sockaddr_in saddr, caddr;memset(&saddr, 0, sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("192.168.1.124");int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));if (res == -1){printf("bind err\n");exit(1);}res = listen(sockfd, 5); // 设置监听队列的大小if (res == -1){exit(1);}while (1){int len = sizeof(caddr);int c = accept(sockfd, (struct sockaddr *)&caddr, &len); // 没有客户端连接,accept阻塞if (c < 0){continue;}printf("accept c=%d\n", c);pthread_t id;int * p = (int*)malloc(sizeof(int));if( p == NULL ){close(c);continue;}*p = c;pthread_create(&id,NULL,fun,(void*)p);}
}多线程并发的核心问题与解决方案
多线程模型虽简单,但在高并发或复杂场景下会暴露以下问题,需针对性解决:
1. 问题 1:线程创建销毁开销大(高并发瓶颈)
现象:客户端连接数达几千时,频繁创建 / 销毁线程会占用大量 CPU 资源,导致服务器响应变慢;
原因:线程是内核级资源,创建时需分配栈空间(默认 8MB)、切换内核态 / 用户态,销毁时需回收资源,高频操作开销显著;
解决方案:使用线程池(提前创建固定数量的空闲线程,复用线程处理多个客户端):
核心逻辑:主线程将新连接放入任务队列,线程池中的空闲线程从队列中获取任务(
client_fd)并处理;优势:降低线程创建销毁开销,控制最大线程数(避免资源耗尽);
实现关键:用互斥锁(
pthread_mutex_t)保护任务队列,用条件变量(pthread_cond_t)唤醒空闲线程。
2. 问题 2:线程安全问题(共享资源竞争)
现象:多个子线程操作共享资源(如全局计数器、数据库连接池、文件)时,会出现数据错乱、死锁等问题;
示例:多个线程同时执行
g_count++(实际是load→add→store三步,可能被线程切换打断),导致计数不准;解决方案:
互斥锁(
pthread_mutex_t):同一时间仅允许一个线程访问共享资源(“加锁→操作→解锁”);读写锁(
pthread_rwlock_t):读多写少场景优化(多个线程可同时读,写时独占);避免共享资源:尽量让线程使用私有数据(局部变量、堆内存),减少共享(最佳实践)。
3. 问题 3:资源耗尽风险(文件描述符 / 内存)
现象:每个线程对应一个
client_fd,每个线程默认占用 8MB 栈内存,连接数过多(如几万)时,会耗尽文件描述符或内存;原因:Linux 默认单个进程最大文件描述符数为 1024,最大线程数受内存限制(8MB / 线程 × 1000 线程 = 8GB 内存);
解决方案:
调整系统参数:
ulimit -n 65535(增大最大文件描述符数)、pthread_attr_setstacksize(减小线程栈大小,如设为 1MB);限制最大连接数:主线程
accept后检查当前连接数,超过阈值则close(client_fd)拒绝连接;改用非阻塞 I/O+epoll:高并发场景(万级以上)用
epoll监听多个客户端,一个线程处理多个连接。
4. 问题 4:线程阻塞导致资源浪费
现象:子线程在
recv(等待数据)、数据库查询、sleep时会阻塞,此时线程无法处理其他任务,CPU 利用率低;原因:阻塞线程会被内核挂起,直到等待事件完成(如数据到达),期间占用线程资源但不干活;
解决方案:
非阻塞 I/O + I/O 多路复用:将
client_fd设为非阻塞(fcntl),用epoll监听多个client_fd的 “可读 / 可写” 事件,一个线程处理多个客户端;异步 I/O:使用
aio_read/aio_write(POSIX 异步 I/O),避免线程阻塞。
5. 问题 5:惊群效应(多线程监听同一端口)
现象:若多个线程同时调用
accept监听同一端口,新连接到达时所有线程会被唤醒,但只有一个线程能成功accept,其他线程白唤醒(浪费 CPU);解决方案:
主线程单独监听:仅主线程
accept接收连接,子线程仅处理业务(推荐,避免惊群);启用
SO_REUSEPORT:Linux 3.9 + 支持,多个线程可绑定同一端口,内核公平分配新连接。
线程池优化(解决高并发瓶颈)
为解决线程创建销毁开销问题,以下是简化版线程池的核心实现,结合上述多线程服务器:
1. 线程池核心结构
// 任务队列节点(存储客户端连接信息)
typedef struct Task {int client_fd;struct sockaddr_in client_addr;struct Task* next;
} Task;// 线程池结构
typedef struct ThreadPool {pthread_t* threads; // 线程数组Task* task_queue; // 任务队列pthread_mutex_t mutex; // 保护任务队列的互斥锁pthread_cond_t cond; // 唤醒空闲线程的条件变量int thread_num; // 线程池大小int is_shutdown; // 线程池是否关闭
} ThreadPool;
2. 线程池初始化与任务添加
// 线程池初始化(创建thread_num个工作线程)
ThreadPool* thread_pool_init(int thread_num) {ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));pool->thread_num = thread_num;pool->is_shutdown = 0;pool->task_queue = NULL;// 初始化互斥锁和条件变量pthread_mutex_init(&pool->mutex, NULL);pthread_cond_init(&pool->cond, NULL);// 创建工作线程pool->threads = (pthread_t*)malloc(sizeof(pthread_t)*thread_num);for (int i=0; i<thread_num; i++) {pthread_create(&pool->threads[i], NULL, thread_pool_worker, pool);pthread_detach(pool->threads[i]); // 线程分离,自动回收}return pool;
}// 向线程池添加任务(主线程调用)
void thread_pool_add_task(ThreadPool* pool, int client_fd, struct sockaddr_in client_addr) {// 创建新任务Task* new_task = (Task*)malloc(sizeof(Task));new_task->client_fd = client_fd;new_task->client_addr = client_addr;new_task->next = NULL;// 加锁保护任务队列pthread_mutex_lock(&pool->mutex);// 将任务加入队列尾部Task* tmp = pool->task_queue;if (tmp == NULL) {pool->task_queue = new_task;} else {while (tmp->next != NULL) tmp = tmp->next;tmp->next = new_task;}pthread_cond_signal(&pool->cond); // 唤醒一个空闲线程pthread_mutex_unlock(&pool->mutex);
}// 线程池工作函数(子线程执行)
void* thread_pool_worker(void* arg) {ThreadPool* pool = (ThreadPool*)arg;while (1) {pthread_mutex_lock(&pool->mutex);// 队列空且未关闭时,阻塞等待任务while (pool->task_queue == NULL && !pool->is_shutdown) {pthread_cond_wait(&pool->cond, &pool->mutex);}// 线程池关闭,退出线程if (pool->is_shutdown) {pthread_mutex_unlock(&pool->mutex);pthread_exit(NULL);}// 取出队列头部任务Task* task = pool->task_queue;pool->task_queue = task->next;pthread_mutex_unlock(&pool->mutex);// 处理任务(复用之前的client_handler逻辑)client_handler_task(task->client_fd, task->client_addr);free(task); // 释放任务内存}
}
3. 主线程修改(使用线程池)
int main() {int listen_fd = socket_init("192.168.1.124", 6000);if (listen_fd == -1) exit(EXIT_FAILURE);// 初始化线程池(创建5个工作线程,可根据CPU核心数调整)ThreadPool* pool = thread_pool_init(5);while (1) {struct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);if (client_fd == -1) {if (errno == EINTR) continue;perror("accept failed");continue;}// 向线程池添加任务(而非创建新线程)thread_pool_add_task(pool, client_fd, client_addr);}
}
多线程并发的选型建议
| 并发模型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 一连接一线程 | 实现简单、客户端隔离、开发成本低 | 线程开销大、高并发瓶颈 | 中低并发(≤1000 连接)、业务逻辑复杂(如耗时计算、数据库查询) |
| 线程池 + 多线程 | 降低线程开销、控制资源占用 | 需实现线程池(复杂)、任务队列可能成为瓶颈 | 中高并发(1000~10000 连接)、CPU 密集型业务 |
| epoll + 多线程(Reactor) | 资源消耗低、支持万级高并发 | 实现复杂(需处理事件分发、线程安全) | 高并发(≥10000 连接)、I/O 密集型业务(如 API 服务器、网关) |
选型核心原则:
若连接数少(≤1000)、业务逻辑复杂:选 “一连接一线程”(简单高效);
若连接数中等(1000~10000)、需控制资源:选 “线程池”;
若连接数多(≥10000)、I/O 密集:选 “epoll + 线程池”(Reactor 模型)。
总结
多线程处理并发的核心价值是 “简单直观、客户端隔离、支持多核”,适合中低并发场景。实际开发中需重点关注:
线程参数传递:用堆内存避免栈地址失效;
资源释放:线程分离(
pthread_detach)、关闭client_fd、释放堆内存;线程安全:共享资源需用互斥锁 / 读写锁保护;
高并发优化:用线程池复用线程,或结合
epoll实现 I/O 多路复用。
