Linux 学习笔记(四十一):TCP 并发演进——从单进程到多线程服务器

📌 核心结论(TL;DR):

  1. 单进程阻塞 :单进程 TCP 服务器卡在 read 时,无法 accept 新连接。

  2. 多进程方案 (fork):主进程接客,子进程服务。优点是隔离性好,缺点是资源开销极大。

  3. 多线程方案 (pthread) :主线程 accept,子线程 read/write。开销小,并发量高。

  4. 多线程两大坑 :必须调用 pthread_detach 回收线程资源(防僵尸线程);主线程必须 close(conn_fd) 防止文件描述符泄漏。

一、 为什么需要并发服务器?

最初的 EchoServer,一旦 accept 成功,主线程就陷入了 while(read) 阻塞,此时其他客户端根本无法建立连接。

二、 多线程并发模型设计

采用经典的 主从模式:

  • 主线程(Main Thread) :死循环执行 accept,负责"拉客"。

  • 工作线程(Worker Thread) :每来一个客户端,主线程就创建一个新线程,并把 connfd 交给它。工作线程负责 read/write 和关闭连接。

三、 代码实战与核心避坑

(插入 thread_worker 函数的核心代码和主线程创建线程的代码)

cpp 复制代码
// ... 省略部分结构体定义
    while (1) {
        ThreadArg* arg = (ThreadArg*)malloc(sizeof(ThreadArg));
        socklen_t len = sizeof(arg->client_addr);
        arg->conn_fd = accept(listen_fd, (struct sockaddr*)&(arg->client_addr), &len);
        
        pthread_t tid;
        pthread_create(&tid, NULL, thread_worker, (void*)arg);
        
        // 🚨 坑点 1:分离线程
        pthread_detach(tid); 
        
        // 🚨 坑点 2:关闭主线程的 conn_fd
        close(arg->conn_fd); 
    }

深度解析:

  1. 为什么要 pthread_detach?如果不分离,线程结束后资源不回收,系统会报 pthread_create failed: Resource temporarily unavailable。

  2. 为什么主线程必须 close(conn_fd)?因为 accept 返回的文件描述符是主线程拥有的,传给子线程后,如果不关闭主线程的,主线程的文件描述符表会无限增长,导致 Too many open files。

四、 进阶预告

多线程虽然好,但如果客户端达到上万个,创建上万个线程依然会压垮 CPU。

自我实战操作

多线程版服务端 tcp_server_thread.c
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>

typedef struct {
    int conn_fd;
    struct sockaddr_in client_addr;
} ThreadArg;

// 子线程干活的函数
void* thread_worker(void* arg) {
    ThreadArg* t_arg = (ThreadArg*)arg;
    int conn_fd = t_arg->conn_fd;
    char buffer[1024];
    
    printf("新线程开始服务客户端: %d\n", conn_fd);
    while (1) {
        int n = read(conn_fd, buffer, sizeof(buffer) - 1);
        if (n > 0) {
            buffer[n] = '\0';
            write(conn_fd, buffer, n); // 回显
        } else if (n == 0) {
            printf("客户端 %d 断开\n", conn_fd);
            break;
        } else break;
    }
    close(conn_fd); // 子线程负责关闭自己通信的 FD
    free(t_arg);
    return NULL;
}

int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(9999);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    
    bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(listen_fd, 10);
    printf("多线程 TCP 服务器启动...\n");

    while (1) {
        ThreadArg* arg = (ThreadArg*)malloc(sizeof(ThreadArg));
        socklen_t len = sizeof(arg->client_addr);
        
        // 主线程只负责接客
        arg->conn_fd = accept(listen_fd, (struct sockaddr*)&(arg->client_addr), &len);
        
        // 创建子线程去处理通信
        pthread_t tid;
        pthread_create(&tid, NULL, thread_worker, (void*)arg);
        
        // 🚨 核心坑点 1:一定要分离线程!
        pthread_detach(tid); 
        
        // 🚨 核心坑点 2:主线程必须关掉 conn_fd!因为已经复制给子线程了。
        // 如果不关,主线程的 FD 会越来越多,最后爆满。
        close(arg->conn_fd); 
    }
    return 0;
}
相关推荐
ynchyong1 小时前
Linux 有两个 “版本“
linux·发行版·内核版本
叼烟扛炮1 小时前
Linux 学习笔记(四十四):HTTP 协议解析与极简 Web 服务器实战
linux·服务器·学习
叼烟扛炮1 小时前
Linux 学习笔记(四十三):网络编程进阶——自定义协议解决 TCP 粘包与守护进程
linux·开发语言·网络·笔记·tcp/ip
Decay__1 小时前
09_MySQL 闪回与时间点恢复 —— binlog 回放、mysqlbinlog、PITR 与误删恢复
运维·数据库·sql·mysql·架构
久曲健的测试窝2 小时前
2026软件测试工具选型指南:双模式如何化解UI脚本维护难题
运维·人工智能·科技·aigc
Escalating_xu2 小时前
【Linux 网络编程】DNS 与 ICMP:从域名解析到 ping、traceroute,把“网络通不通”问清楚
linux·网络·php
songyuc2 小时前
【Manskill3】TableSceneBuilder学习笔记
pytorch·笔记·学习
外收内放2 小时前
Python基础语法练习题(75)
开发语言·python·学习
NailiConveyor3 小时前
柔性物流方案如何适配工厂升级?自动化输送流水线定制要点
大数据·运维·自动化·制造·业界资讯