14、多线程并发服务器:使用pthread_create()创建线程处理多个客户端连接

说实话,多线程并发服务器这块,我当年刚入行时也踩过不少坑。那时候公司有个项目,需要同时处理几百个客户端连接,我一开始用的是多进程方案——fork()一把梭,结果内存开销大得吓人。后来老工程师跟我说:「你小子试试多线程啊,轻量多了。」嗯,从那以后,我就跟pthread杠上了。

为什么需要多线程?

你想想看,一个单线程的服务器,一次只能服务一个客户端。如果某个客户端发完请求后磨磨蹭蹭不读响应,后面的客户端就得排队等着。这在生产环境里根本没法用。

多线程方案的核心思路是:主线程负责accept(),每来一个客户端连接,就创建一个新线程去处理。这样各个客户端之间互不干扰,谁慢谁自己扛,不影响别人。

核心要点:多线程服务器 = 主线程监听 + 工作线程处理。每个客户端连接对应一个独立线程。

pthread_create() 的基本用法

先看函数原型,这个我闭着眼都能写出来:

#include <pthread.h>

int pthread_create(pthread_t *thread,
                   const pthread_attr_t *attr,
                   void *(*start_routine)(void *),
                   void *arg);

参数说明:

  • thread:线程ID的指针,创建成功后这里会填上线程ID
  • attr:线程属性,传NULL就用默认属性
  • start_routine:线程要执行的函数,格式必须是 void* func(void*)
  • arg:传给线程函数的参数

返回值:成功返回0,失败返回错误码。

个人习惯:我一般把客户端socket的文件描述符通过arg参数传给线程函数。注意要传指针,别直接传int值——虽然32位系统上能凑合用,但64位系统上int和指针长度不一样,会出问题。

一个完整的多线程并发服务器

下面这个例子,是我早期项目里提炼出来的骨架代码。它实现了最基本的「来一个客户端,开一个线程」模型。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>

#define PORT 8888
#define MAX_CLIENTS 100

// 处理客户端通信的线程函数
void *handle_client(void *arg) {
    int client_fd = *(int *)arg;
    free(arg);  // 释放动态分配的内存
    pthread_detach(pthread_self());  // 分离线程,自动回收资源

    char buffer[1024];
    int bytes_read;

    while ((bytes_read = read(client_fd, buffer, sizeof(buffer) - 1)) > 0) {
        buffer[bytes_read] = '\0';
        printf("收到消息: %s\n", buffer);

        // 回显给客户端
        write(client_fd, buffer, bytes_read);
    }

    printf("客户端断开连接\n");
    close(client_fd);
    return NULL;
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);

    // 1. 创建socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket创建失败");
        exit(1);
    }

    // 2. 绑定地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind失败");
        exit(1);
    }

    // 3. 监听
    if (listen(server_fd, MAX_CLIENTS) < 0) {
        perror("listen失败");
        exit(1);
    }

    printf("服务器启动,监听端口 %d...\n", PORT);

    // 4. 循环接受连接
    while (1) {
        client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
        if (client_fd < 0) {
            perror("accept失败");
            continue;
        }

        printf("新客户端连接: %s:%d\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));

        // 动态分配内存,保存client_fd
        int *pclient = malloc(sizeof(int));
        *pclient = client_fd;

        pthread_t tid;
        if (pthread_create(&tid, NULL, handle_client, pclient) != 0) {
            perror("线程创建失败");
            close(client_fd);
            free(pclient);
        }
    }

    close(server_fd);
    return 0;
}

代码里的几个关键点

这段代码看着简单,但里面有几个细节,我曾经都吃过亏:

  1. 动态分配内存传参:我把client_fd包装成malloc出来的int指针传给线程。为什么?因为如果直接传&client_fd的地址,主线程下次循环时这个变量的值就变了,新线程拿到的可能是错的。
  2. pthread_detach():线程函数里第一件事就是detach自己。这样线程结束后系统会自动回收资源,主线程不用pthread_join()去等它。省心。
  3. free(arg):传进去的指针用完了要释放,不然内存泄漏。这个坑我踩过——有一次线上服务器跑了三天,内存涨了2个G,查了半天才发现是这里忘了free。

我曾经犯过的错:一开始我图省事,直接把client_fd强转成void*传进去:pthread_create(&tid, NULL, handle_client, (void*)client_fd)。这在32位系统上没问题,但64位系统上int是4字节,指针是8字节,编译会报警告,而且高4字节是垃圾值。后来线程里拿到的fd值完全不对,连接全挂了。切记:传指针,不要传值。

多线程服务器的架构图

下面这张图,是我自己画的多线程服务器工作流程。你看一眼就能明白整体逻辑:

主线程 socket() → bind() → listen() while(1) accept() 等待连接 pthread_create() 工作线程 1 处理客户端 A 工作线程 2 处理客户端 B 工作线程 3 处理客户端 C 线程自动回收(pthread_detach)

这张图里,主线程一直在accept()循环里转,每接到一个客户端就开一个工作线程。工作线程处理完就自己结束,资源自动回收。说白了就是「主线程只管接客,工作线程负责伺候」。

多线程 vs 多进程:怎么选?

我经常被问到这个问题。直接上对比表:

对比项 多线程 多进程
创建开销 低(共享地址空间) 高(需要复制页表)
内存占用 低(共享全局变量) 高(每个进程独立内存)
同步难度 高(需要加锁) 低(进程间隔离)
崩溃影响 一个线程崩溃 → 整个进程挂 一个进程崩溃 → 不影响其他进程
适用场景 高并发、轻量级任务 需要强隔离、稳定性要求高

我个人习惯是:内部服务用多线程,对外服务用多进程。内部服务可控,线程挂了能快速定位;对外服务要稳,一个客户端搞崩了不能影响其他人。

避坑指南:多线程服务器的常见问题

嗯,这里我总结几个实战中经常遇到的问题:

  • 线程数量无限制增长:如果客户端频繁连接断开,线程会不断创建销毁。建议设置最大线程数,超过就拒绝连接或放入队列等待。
  • 共享数据竞争:多个线程同时操作全局变量时,记得加互斥锁。我见过一个项目,日志打印函数里没加锁,结果两个线程同时写日志,输出全乱套了。
  • 文件描述符泄漏:每个线程里close()别忘了。如果线程异常退出,fd没关,服务器跑几天就会报"Too many open files"。
  • 信号处理:多线程程序里信号处理很麻烦。建议主线程屏蔽所有信号,工作线程不处理信号,统一由专门的信号线程处理。

一个小技巧:如果你不想手动管理线程生命周期,可以考虑用线程池。提前创建一批线程,有任务来了就丢给空闲线程处理。这样避免了频繁创建销毁的开销。不过线程池的实现比较复杂,我们后面章节会专门讲。

总结

多线程并发服务器,说白了就是「主线程接客,工作线程干活」的模型。pthread_create()是核心API,但真正用好它,需要处理好参数传递、资源回收、同步互斥这些细节。

我刚开始写多线程程序时,总觉得不就是开个线程嘛,有什么难的?结果线上出了好几次事故。后来慢慢养成了习惯:每创建一个线程,都要问自己三个问题——参数传对了没?资源谁回收?共享数据锁了没?

这三个问题想清楚了,你的多线程服务器基本就稳了。


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