线程池:设计原理、实现与任务调度

线程池这东西,说白了就是「提前招一批工人等着干活」。

我刚开始做并发编程时,总觉得来一个任务就 new 一个线程挺爽的。直到有一次线上服务被流量冲垮——每秒几千个请求,每个请求都创建线程,系统直接 OOM 了。嗯,从那以后,线程池就成了我工具箱里的标配。

为什么需要线程池?

你想想看,线程的创建和销毁是有代价的:

  • 创建线程需要分配栈空间、初始化内核资源
  • 销毁线程需要回收资源、通知内核
  • 频繁创建销毁会导致频繁的系统调用

线程池的核心思想就是:复用。提前创建好一批线程,任务来了直接丢给它们处理,处理完线程不销毁,继续等下一个任务。

一句话总结:线程池 = 预先创建的线程集合 + 任务队列 + 调度策略

线程池的设计原理

我习惯把线程池拆成三个核心组件来看:

  1. 线程管理者:负责线程的创建、销毁、扩容、缩容
  2. 任务队列:存放待执行的任务,通常是线程安全的队列
  3. 调度策略:决定任务如何分配给线程,以及线程池如何调整大小

这里我画了一张图,帮你理清它们之间的关系:

任务队列 Task1 | Task2 | Task3 (线程安全队列) 调度器 分配策略 (负载均衡) 线程池 Thread 1 Thread 2 Thread 3 (可扩容/缩容) 结果 监控 & 调整

线程池的实现

在 C 语言里实现一个线程池,我通常会用到 pthread 库。下面是一个简化版的实现思路:

// 线程池结构体
typedef struct {
    pthread_t *threads;      // 线程数组
    task_queue_t *queue;     // 任务队列
    int thread_count;        // 线程数量
    int shutdown;            // 关闭标志
    pthread_mutex_t lock;    // 互斥锁
    pthread_cond_t notify;   // 条件变量
} thread_pool_t;

// 工作线程函数
void *worker(void *arg) {
    thread_pool_t *pool = (thread_pool_t *)arg;
    
    while (1) {
        pthread_mutex_lock(&pool->lock);
        
        // 队列为空时等待
        while (pool->queue->count == 0 && !pool->shutdown) {
            pthread_cond_wait(&pool->notify, &pool->lock);
        }
        
        if (pool->shutdown) {
            pthread_mutex_unlock(&pool->lock);
            pthread_exit(NULL);
        }
        
        // 取出任务
        task_t task = dequeue(pool->queue);
        pthread_mutex_unlock(&pool->lock);
        
        // 执行任务
        task.function(task.arg);
    }
    return NULL;
}

我的经验:条件变量这里容易踩坑。一定要用 while 循环检查条件,而不是 if。因为 pthread_cond_wait 可能被「虚假唤醒」——这是 POSIX 标准允许的。我曾经因为这个 bug 排查了整整两天。

线程池的任务调度

任务调度是线程池的灵魂。我见过几种常见的调度策略:

策略名称 原理 适用场景
FIFO(先进先出) 任务按到达顺序执行 通用场景,公平性好
优先级调度 高优先级任务先执行 实时系统、关键任务优先
工作窃取 空闲线程从其他线程队列偷任务 负载不均衡的场景
动态扩容 根据队列长度动态增减线程数 流量波动大的服务

我个人最常用的是「动态扩容 + FIFO」的组合。为什么呢?因为大多数业务场景下,任务的执行时间差异不大,FIFO 足够简单高效。动态扩容则能应对突发流量。

避坑指南

我曾经犯过的错:

  • 线程数设得太大:以为线程越多越快,结果上下文切换开销把 CPU 吃满了。经验值:CPU 密集型任务,线程数 = CPU 核心数 + 1;IO 密集型任务,可以适当多些。
  • 任务队列无界:任务积压太多,内存爆了。一定要用有界队列,配合拒绝策略。
  • 忘记处理 shutdown:程序退出时线程池没正确关闭,导致资源泄漏。

一个完整的调度示例

下面是一个带动态扩容的调度逻辑片段:

void thread_pool_schedule(thread_pool_t *pool, task_t task) {
    pthread_mutex_lock(&pool->lock);
    
    // 队列满了,考虑扩容
    if (queue_is_full(pool->queue)) {
        if (pool->thread_count < pool->max_threads) {
            // 创建一个新线程
            pthread_t new_thread;
            pthread_create(&new_thread, NULL, worker, pool);
            pool->threads[pool->thread_count++] = new_thread;
        } else {
            // 执行拒绝策略
            reject_task(task);
            pthread_mutex_unlock(&pool->lock);
            return;
        }
    }
    
    enqueue(pool->queue, task);
    pthread_cond_signal(&pool->notify);
    pthread_mutex_unlock(&pool->lock);
}

核心要点:扩容和缩容都要有阈值。我一般设两个阈值:队列长度超过 80% 时扩容,低于 20% 时缩容。避免频繁抖动。

总结一下

线程池不是什么高深的技术,但用好了能省很多事。记住三个关键点:

  • 线程数要合理:不是越多越好,要根据任务类型调整
  • 队列要有界:防止内存被撑爆
  • 调度策略要匹配业务:没有银弹,适合的才是最好的

嗯,线程池的内容就聊到这里。实际项目中,你还会遇到线程池的监控、动态配置、优雅关闭等问题,这些都是在实践中慢慢积累的。

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