当前位置: 首页 > news >正文

Linux33 网络编程-多线程TCP并发

多线程并发是网络编程中处理多客户端连接的核心方案,核心思想是主线程负责监听连接,子线程为每个客户端提供专属服务—— 通过线程的独立性隔离客户端,同时利用 CPU 多核提升处理效率,适合中低并发(几百~几千连接)、业务逻辑复杂的场景(如 SSH、文件传输、数据库查询)。以下从原理、实现、问题、优化展开,结合实战代码深度解析:

核心原理:多线程并发的工作模型

多线程并发基于 “分工协作”,主线程与子线程各司其职,通过内核调度实现并发执行:

1. 核心角色与流程

线程类型核心职责关键操作
主线程(监听线程)接收客户端连接,分配任务socket()bind()listen()accept()→创建子线程
子线程(工作线程)与单个客户端全量交互

接收数据(recv)→ 业务处理 → 发送响应(send)→ 关闭连接(close

2. 并发实现的核心逻辑

  1. 主线程启动后,初始化监听套接字并绑定端口,进入循环等待客户端连接;

  2. 每收到一个新连接(accept()返回客户端套接字client_fd),创建一个子线程;

  3. 子线程接收client_fd,与对应客户端单独交互(收发数据、业务处理),不影响其他线程;

  4. 客户端断开连接后,子线程释放资源(关闭client_fd、退出线程);

  5. 主线程持续监听新连接,实现 “同时处理多个客户端” 的效果。

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 模型)。

总结

多线程处理并发的核心价值是 “简单直观、客户端隔离、支持多核”,适合中低并发场景。实际开发中需重点关注:

  1. 线程参数传递:用堆内存避免栈地址失效;

  2. 资源释放:线程分离(pthread_detach)、关闭client_fd、释放堆内存;

  3. 线程安全:共享资源需用互斥锁 / 读写锁保护;

  4. 高并发优化:用线程池复用线程,或结合epoll实现 I/O 多路复用。

http://www.dtcms.com/a/589621.html

相关文章:

  • Tomcat和负载均衡
  • 【算力】AI万卡GPU集群交付确认项与日常运维(算力压测、数据倒腾、日常运维)
  • 网站建设 东八区学校网站建设的意义的主要负责人
  • 网站开发招商计划书c 网站开发框架有
  • 成都企业网站开发网站主页设计费用
  • 数据结构——四十、折半查找(王道408)
  • 操作系统 内存(5)虚拟内存机制
  • 郑州网站建设专业乐云seowordpress user role
  • JavaScript 的 Web APIs 入门到实战全总结(day7):从数据处理到交互落地的全链路实战(附实战案例代码)
  • 分类型网站建设付费推广外包
  • 17_FastMCP 2.x 中文文档之FastMCP服务端高级功能:LLM采样详解
  • 集团网站建设制作费用百度公司是国企还是私企
  • Go Channel 深度指南:规范、避坑与开源实践
  • Postman 脚本控制特定请求的执行流程(跳过执行)
  • Kubernetes Deployment 控制器
  • 网络体系结构-物理层
  • 色彩搭配 网站无障碍网站建设方案
  • 网站建设制作公一般做个网站多少做网站多少钱
  • 商业网站建站目的官网建站系统
  • HCCDE-GaussDB相关计算题
  • 从SOMEIP看SOA,汽车电子电器架构的转变
  • 免费自己制作logo的网站wordpress百度百科
  • asp制作网站教程猎头公司网站素材
  • Java--JVM
  • 英语学习——单词篇(第十七天)
  • 福州做网站wordpress修改footer
  • 顺序表vector--------练习题9题解
  • 深入浅出:低噪声放大器(LNA)与USB芯片——无线与有线通信的基石
  • C++线程操作
  • 培训网站网站建设上海 网站建设google