通过引入多线程,我们完美解决了刚才提到的“独占服务器”问题,实现了既能长连接(不 close),又不耽误接待新客户

1. 这个模型的逻辑是这样的:

  1. 主线程:只负责做“迎宾”工作。它在 while 循环里不断 accept
  2. 子线程:一旦 accept 返回一个 client_fd,主线程立马把这个 fd 扔给一个新创建的子线程,然后主线程转头继续去 accept 下一个。
  3. 各忙各的:子线程自己在那个函数里跑 while(recv/send),不管聊多久,都不会卡住主线程。

2. 代码结构示意

// 定义子线程要执行的函数
void *handle_client(void *arg) {
    int client_fd = *(int*)arg;
    
    // 子线程:死磕这个客户,长连接模式
    while (1) {
        int ret = recv(client_fd, buffer, ...);
        if (ret <= 0) break; // 客户走了或出错了
        
        // 处理业务
        send(client_fd, response, ...);
    }
    
    // 客户走了,子线程负责关闭 fd 并退出
    close(client_fd);
    return NULL;
}

int main() {
    // 初始化 socket, bind, listen...
    
    while (1) {
        int client_fd = accept(server_fd, ...);
        
        // 创建新线程,把 client_fd 传进去
        pthread_t tid;
        pthread_create(&tid, NULL, handle_client, &client_fd);
        
        // 主线程不管了,立刻回到循环开头接待下一个
    }
}

3. 虽然可行,但有三个“坑”要注意

虽然“起一个线程”听起来很简单,但在实际的高并发服务器开发中,直接这样写会有隐患:

A. 资源消耗(C10K 问题)

如果你的服务器只有几十、几百个连接,这个方法非常完美。
但如果有一万个客户端同时连上来(C10K 问题):

  • 内存爆炸:每个线程默认栈空间可能是 8MB 或 10MB。10000 个线程 ≈\approx 10GB 内存,服务器直接撑爆。
  • CPU 抖颤:CPU 在上万个线程之间切换,系统会变得非常慢。
B. 线程安全(竞态条件)

如果多个线程(对应多个客户)同时操作同一个资源(比如全局变量、<u>同一个文件</u>、或者数据库连接池),你需要加锁,否则数据会乱套。

C. 文件描述符传递的坑

在上面的伪代码中,pthread_create 传的是 &client_fd 的地址。这是一个非常经典的 Bug。

  • 问题:主线程循环很快,可能在子线程读取 client_fd 之前,主线程已经修改了 client_fd(指向了下一个客户)。
  • 解决:应该动态 malloc 一块内存存 client_fd,传给子线程,或者直接传值(如果指针大小能存下 int)。

4. 更好的替代方案

为了解决“线程太多资源不够”的问题,现代高性能服务器通常采用以下两种方案之一:

  1. 线程池

    • 不要来一个客户就起一个线程。
    • 预先创建好比如 4 个或 8 个线程。
    • client_fd 放进一个任务队列里,空闲的线程自己去队列里取任务处理。
  2. IO 多路复用

    • 一个线程就能管理上万个连接。
    • 它不阻塞在 recv 上,而是阻塞在 select/epoll 上。哪个 fd 有数据来了,就去处理哪个。

总结

  • 你的想法是对的:多线程确实解决了“不 close 导致无法 accept”的矛盾。
  • 适用场景:适合连接数不多、逻辑比较复杂的场景。
  • 进阶思考:如果连接数成千上万,记得用线程池epoll来优化。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐