手撕线程池:巩固Linux线程知识

目录

引言

[1. 核心数据结构:先把架子搭起来](#1. 核心数据结构:先把架子搭起来)

[2. 三步实现法:从能用→好用→优雅](#2. 三步实现法:从能用→好用→优雅)

第一步:基础版本(固定线程数,能跑就行)

第二步:添加动态扩容(进阶)

第三步:优化设计(无锁队列、优先级、监控接口)

[3. 测试用例:](#3. 测试用例:)

[4. 常见难点与解决方案](#4. 常见难点与解决方案)

关键知识点:

[5. 扩展思考:线程池还能怎么玩?](#5. 扩展思考:线程池还能怎么玩?)


引言

兄弟们,学完Linux线程(pthread)、互斥锁、条件变量,是不是感觉理论知识都会了,但真要动手写个并发程序,还是有点虚?别慌,今天咱们就来干一件最实在的事:手撕一个线程池

为什么要手撕线程池?因为它是把"生产者-消费者模型"、"线程同步"、"资源管理"这些硬核知识点串起来的最佳实践。你写一遍,胜过看十遍书。而且,面试官最爱问这个。

话不多说,直接开干!我们的目标是:从零开始,写出一个能用的C语言线程池,并逐步让它变强。

1. 核心数据结构:先把架子搭起来

一切代码的基础是数据结构(先描述再组织 )。线程池的核心就两个结构体:任务(task_t)和线程池本身(thread_pool_t)。

cpp 复制代码
// task.h
#ifndef TASK_H
#define TASK_H

// 任务结构体
typedef struct task {
    void (*function)(void* arg); // 任务函数指针
    void* arg;                   // 函数参数
    struct task* next;           // 链表指针,用于构建任务队列
} task_t;

#endif
cpp 复制代码
// thread_pool.h
#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <pthread.h>
#include "task.h"

// 线程池结构体
typedef struct thread_pool {
    task_t* task_queue;          // 任务队列头指针(链表实现)
    pthread_mutex_t queue_mutex; // 保护任务队列的互斥锁
    pthread_cond_t queue_cond;   // 条件变量,用于线程等待/唤醒
    pthread_t* threads;          // 工作线程ID数组
    int thread_count;            // 线程池中线程数量
    int shutdown;                // 关闭标志:0-运行,1-关闭
} thread_pool_t;

// 函数声明
thread_pool_t* thread_pool_create(int thread_count);
void thread_pool_destroy(thread_pool_t* pool);
void thread_pool_submit(thread_pool_t* pool, void (*func)(void*), void* arg);

#endif

关键点说明:

  • 任务队列 :用单向链表实现,简单高效。task_t 就是链表节点。
  • 互斥锁 queue_mutex :保护 task_queue 这个共享资源,防止多个线程同时操作导致数据错乱。
  • 条件变量 queue_cond:当任务队列为空时,工作线程通过它进入等待状态;当有新任务加入时,通过它唤醒一个等待的线程。
  • shutdown 标志:优雅关闭线程池的关键。

2. 三步实现法:从能用→好用→优雅

第一步:基础版本(固定线程数,能跑就行)

这是最核心的部分,我们直接上完整代码。

cpp 复制代码
// thread_pool.c
#include "thread_pool.h"
#include <stdlib.h>
#include <stdio.h>

// 工作线程执行的函数
static void* worker(void* arg) {
    thread_pool_t* pool = (thread_pool_t*)arg;

    while (1) {
        pthread_mutex_lock(&pool->queue_mutex);

        // 条件变量等待:队列为空且未关闭时等待
        while (pool->task_queue == NULL && !pool->shutdown) {
            pthread_cond_wait(&pool->queue_cond, &pool->queue_mutex);
        }

        // 如果关闭且任务队列为空,线程退出
        if (pool->shutdown && pool->task_queue == NULL) {
            pthread_mutex_unlock(&pool->queue_mutex);
            pthread_exit(NULL);
        }

        // 取出队列头部的任务
        task_t* task = pool->task_queue;
        pool->task_queue = task->next;

        pthread_mutex_unlock(&pool->queue_mutex);

        // ️ 执行任务时,已经释放了锁!这是关键设计
        task->function(task->arg);
        free(task);
    }
    return NULL;
}

// 创建线程池
thread_pool_t* thread_pool_create(int thread_count) {
    thread_pool_t* pool = (thread_pool_t*)malloc(sizeof(thread_pool_t));
    pool->task_queue = NULL;
    pool->thread_count = thread_count;
    pool->shutdown = 0;

    pthread_mutex_init(&pool->queue_mutex, NULL);
    pthread_cond_init(&pool->queue_cond, NULL);

    pool->threads = (pthread_t*)malloc(thread_count * sizeof(pthread_t));
    for (int i = 0; i < thread_count; i++) {
        pthread_create(&pool->threads[i], NULL, worker, pool);
    }
    return pool;
}

// 提交任务
void thread_pool_submit(thread_pool_t* pool, void (*func)(void*), void* arg) {
    task_t* new_task = (task_t*)malloc(sizeof(task_t));
    new_task->function = func;
    new_task->arg = arg;
    new_task->next = NULL;

    pthread_mutex_lock(&pool->queue_mutex);

    // 将新任务添加到队列尾部
    if (pool->task_queue == NULL) {
        pool->task_queue = new_task;
    } else {
        task_t* current = pool->task_queue;
        while (current->next != NULL) {
            current = current->next;
        }
        current->next = new_task;
    }

    // 唤醒一个等待的工作线程
    pthread_cond_signal(&pool->queue_cond);
    pthread_mutex_unlock(&pool->queue_mutex);
}

// 销毁线程池
void thread_pool_destroy(thread_pool_t* pool) {
    pthread_mutex_lock(&pool->queue_mutex);
    pool->shutdown = 1;
    // 广播唤醒所有线程,让它们检查shutdown标志并退出
    pthread_cond_broadcast(&pool->queue_cond);
    pthread_mutex_unlock(&pool->queue_mutex);

    // 等待所有线程结束
    for (int i = 0; i < pool->thread_count; i++) {
        pthread_join(pool->threads[i], NULL);
    }

    // 清理剩余任务
    task_t* current = pool->task_queue;
    while (current != NULL) {
        task_t* next = current->next;
        free(current);
        current = next;
    }

    free(pool->threads);
    pthread_mutex_destroy(&pool->queue_mutex);
    pthread_cond_destroy(&pool->queue_cond);
    free(pool);
}

基础版本核心逻辑:

  1. worker 函数:死循环,加锁检查队列,无任务则等待,有任务则取出执行。
  2. thread_pool_submit:加锁,将任务添加到队列尾部,解锁,并唤醒一个线程。
  3. 关键设计:任务执行在锁外,避免长时间持有锁导致其他线程饥饿。

第二步:添加动态扩容(进阶)

基础版线程数是固定的。如果任务突然暴增,固定线程数可能处理不过来。我们可以添加动态扩容功能。

cpp 复制代码
// 在 thread_pool_t 结构体中添加字段
typedef struct thread_pool {
    // ... 其他字段
    int min_threads;             // 最小线程数
    int max_threads;             // 最大线程数
    int idle_threads;            // 当前空闲线程数
    int active_threads;          // 当前活跃线程数
    // ... 其他字段
} thread_pool_t;

// 管理线程函数(在单独的线程中运行)
static void* manager(void* arg) {
    thread_pool_t* pool = (thread_pool_t*)arg;
    while (!pool->shutdown) {
        sleep(2); // 每隔2秒检查一次

        pthread_mutex_lock(&pool->queue_mutex);
        int queue_size = get_queue_size(pool); // 需要实现一个获取队列长度的函数
        int idle = pool->idle_threads;
        int total = pool->active_threads;
        pthread_mutex_unlock(&pool->queue_mutex);

        // 扩容条件:任务队列长度 > 空闲线程数,且未达到最大线程数
        if (queue_size > idle && total < pool->max_threads) {
            int add_count = (queue_size - idle) < (pool->max_threads - total) ?
                            (queue_size - idle) : (pool->max_threads - total);
            for (int i = 0; i < add_count; i++) {
                pthread_t new_thread;
                pthread_create(&new_thread, NULL, worker, pool);
                // 需要将新线程ID保存到动态数组中(代码略)
            }
            printf("[Manager] Added %d threads. Total: %d\n", add_count, total + add_count);
        }

        // 缩容条件:空闲线程数 > 任务队列长度 * 2,且大于最小线程数
        if (idle > queue_size * 2 && total > pool->min_threads) {
            // 发送"自杀"信号给空闲线程(实现方式:设置标志位或使用额外的条件变量)
            // 这里简化处理,实际需要更复杂的机制
            printf("[Manager] Ready to reduce threads.\n");
        }
    }
    return NULL;
}

注意:动态扩容/缩容的实现比较复杂,需要处理线程ID的动态管理、空闲线程的精确计数、以及如何让空闲线程安全退出等问题。这里只展示核心思路,完整实现留作你的练习。

第三步:优化设计(无锁队列、优先级、监控接口)

对于追求极致性能的场景,可以引入更高级的优化。

优化1:无锁队列

使用CAS(Compare-And-Swap)原子操作实现无锁队列,避免锁竞争,提高性能。但这会大大增加代码复杂度,且容易出错。对于大多数场景,互斥锁已经足够。(这里不做演示)
优化2:任务优先级

将任务队列改为优先队列(如使用堆或有序链表),让高优先级的任务先被执行。

cpp 复制代码
// 在 task_t 中添加优先级字段
typedef struct task {
    void (*function)(void* arg);
    void* arg;
    int priority;                // 优先级,数值越大优先级越高
    struct task* next;
} task_t;

// 提交任务时,按优先级插入到队列中
void thread_pool_submit_priority(thread_pool_t* pool, void (*func)(void*), void* arg, int priority) {
    task_t* new_task = (task_t*)malloc(sizeof(task_t));
    new_task->function = func;
    new_task->arg = arg;
    new_task->priority = priority;
    new_task->next = NULL;

    pthread_mutex_lock(&pool->queue_mutex);

    // 按优先级从高到低插入
    task_t** p = &pool->task_queue;
    while (*p != NULL && (*p)->priority >= priority) {
        p = &(*p)->next;
    }
    new_task->next = *p;
    *p = new_task;

    pthread_cond_signal(&pool->queue_cond);
    pthread_mutex_unlock(&pool->queue_mutex);
}

优化3:监控接口

提供接口查询线程池状态,如当前任务队列长度、活跃线程数、已完成任务数等,方便调优。

cpp 复制代码
// 获取线程池状态
typedef struct pool_status {
    int task_queue_size;
    int active_threads;
    int total_tasks_completed;
} pool_status_t;

pool_status_t thread_pool_get_status(thread_pool_t* pool) {
    pool_status_t status;
    pthread_mutex_lock(&pool->queue_mutex);
    status.task_queue_size = get_queue_size(pool);
    status.active_threads = pool->active_threads;
    status.total_tasks_completed = pool->completed_tasks;
    pthread_mutex_unlock(&pool->queue_mutex);
    return status;
}

3. 测试用例:

cpp 复制代码
// test.c
#include "thread_pool.h"
#include <stdio.h>
#include <unistd.h>

// 一个简单的任务函数:打印任务ID和线程ID
void my_task(void* arg) {
    int task_id = *(int*)arg;
    printf("Task %d is being executed by thread %lu\n", task_id, (unsigned long)pthread_self());
    usleep(100000); // 模拟任务执行耗时 100ms
    free(arg);      // 释放参数内存
}

// 基础测试
void test_basic() {
    printf("=== Test Basic ===\n");
    thread_pool_t* pool = thread_pool_create(4); // 创建4个线程的线程池

    for (int i = 0; i < 10; i++) {
        int* arg = malloc(sizeof(int));
        *arg = i;
        thread_pool_submit(pool, my_task, arg);
    }

    sleep(2); // 等待任务执行完毕
    thread_pool_destroy(pool);
    printf("=== Test Basic Done ===\n");
}

int main() {
    test_basic();
    return 0;
}

编译运行:

cpp 复制代码
gcc -o test test.c thread_pool.c -lpthread -I.
./test

预期输出(线程ID和顺序可能不同):

4. 常见难点与解决方案

难点 现象 解决方案
虚假唤醒 线程被唤醒但队列仍为空,导致访问空指针 使用 while 循环检查 条件,而非 if
死锁 程序卡死,无响应 确保加锁和解锁成对出现;避免在持有锁时调用可能阻塞的函数;使用RAII思想
任务执行耗时 一个耗时任务阻塞了线程,导致其他任务无法被处理 将任务执行放在锁外;考虑为长时间任务单独开线程
优雅关闭 强制关闭导致任务丢失或线程资源泄漏 设置 shutdown 标志;广播唤醒所有线程;pthread_join 等待线程结束

关键知识点:

知识点 一句话总结
互斥锁 保护共享资源(任务队列)的原子访问
条件变量 实现线程的等待与唤醒,避免忙等待
任务队列 生产者和消费者之间的缓冲区
锁外执行 任务执行时释放锁,提高并发度
优雅关闭 设置标志 + 广播唤醒 + 等待线程退出

5. 扩展思考:线程池还能怎么玩?

  1. 任务窃取(Work Stealing):每个线程有自己的任务队列,当某个线程空闲时,可以从其他线程的队列尾部"偷"任务来执行,提高负载均衡。Go语言的GMP模型就采用了类似思想。
  2. CPU密集型 vs IO密集型适配
    • CPU密集型 :线程数通常设置为 CPU核心数 + 1,避免过多线程切换开销。
    • IO密集型 :线程数可以设置得更多,如 CPU核心数 * 2 或更高,因为线程经常在等待IO。
  3. 网络编程集成:将线程池与epoll结合,主线程负责监听和接收新连接,然后将连接处理任务提交给线程池,实现高性能Reactor模型。
相关推荐
basketball6161 小时前
C++ 命名空间知识点总结:从入门到合理设计
开发语言·c++
handler012 小时前
【C++ 算法竞赛基础】数论篇:核心公式、经典例题与高频模板
开发语言·c++·算法·蓝桥杯·数论·最大公约数·最小公倍数
fpcc2 小时前
并行编程实战——CUDA编程的打印输出
c++·cuda
程序leo源2 小时前
Qt信号与槽深度详解
c语言·开发语言·数据库·c++·qt·c#
水云桐程序员2 小时前
C++数组详细介绍
开发语言·c++
z200509302 小时前
今日算法(二叉树)
数据结构·c++·算法
念恒123063 小时前
库制作与原理---库的理解和加载(中)
linux·运维·服务器
宁静@星空3 小时前
009-Linux环境安装宝塔
linux·运维·服务器
故事和你913 小时前
洛谷-【图论2-2】最短路1
开发语言·数据结构·c++·算法·动态规划·图论