线程池

线程池

一、线程池概述

1. 什么是线程池

线程池(Thread Pool)是一种多线程处理形式,它预先创建一定数量的工作线程,并将待处理的任务放入队列中,由这些工作线程循环地从队列中取出任务并执行。线程池属于池式结构(Pooling)的一种,与内存池、连接池等类似,旨在解决资源频繁创建和销毁带来的性能开销问题。

核心思想:通过复用已创建的线程,减少线程创建和销毁的次数,从而提升系统吞吐量和资源利用率。

2. 为什么需要固定数量的线程

线程池中的线程数量通常是固定的,主要原因如下:

  • CPU核心数有限:每个CPU核心在同一时刻只能运行一个线程。如果线程数量超过CPU核心数,会导致频繁的上下文切换,增加操作系统调度负担,反而降低性能。
  • 性能拐点:随着线程数量增加,系统吞吐量先上升后下降。当线程数超过某个临界值(由任务类型、硬件等决定),性能会因资源竞争而下降。
  • 资源管理:固定线程数便于控制资源占用,避免因无限创建线程导致系统资源耗尽(如内存溢出)。

3. 线程池的管理与维持

线程池的管理主要体现在:

  • 任务调度:当有新任务提交时,线程池将任务放入队列。如果存在空闲线程,则唤醒它执行任务;否则任务在队列中等待。
  • 线程状态控制:通过休眠/唤醒机制动态调整线程的活动状态。队列为空时,线程自动休眠释放CPU;队列非空时,唤醒线程处理任务。

二、线程池的组成部分

一个典型的线程池包含两个核心组件:

  1. 线程集合(Thread Pool):一组固定数量的工作线程,负责执行任务。
  2. 任务队列(Task Queue):一个线程安全的队列,用于存储待处理的任务。

1. 线程集合

线程集合的大小(即线程数量)需要根据任务类型合理设置:

  • CPU密集型任务:线程数通常设置为CPU核心数(或核心数+1),避免过多线程竞争CPU。
  • I/O密集型任务:线程数可以设置得更多(例如 2 * CPU核心数 + 2),因为线程在等待I/O时会主动让出CPU,可以利用这段时间执行其他任务。实际最优值需通过压测确定。

2. 任务队列

任务队列是生产者-消费者模型的缓冲区,需要支持高效、线程安全的入队和出队操作。常用实现方式包括:

  • 链表队列:通过头尾指针实现FIFO,入队/出队时间复杂度为O(1)。
  • 环形缓冲区:固定大小,适合任务数量可预估的场景。

队列操作需要加锁以保证线程安全。锁的粒度直接影响性能:

锁类型 特点 适用场景
互斥锁(mutex) 重量级锁,线程阻塞时会进入内核态睡眠 临界区操作耗时较长,线程可能长时间阻塞
自旋锁(spinlock) 轻量级锁,线程忙等待(不释放CPU) 临界区操作极短,线程等待时间短,避免上下文切换

在线程池中,任务队列的操作通常很快(仅指针操作),因此使用自旋锁可以减少线程切换开销。但需注意自旋锁在单核CPU上无意义(会一直占用CPU)。

三、线程池的工作原理

线程池本质上是一个生产者-消费者模型:

  • 生产者线程:负责创建任务并将其放入任务队列。
  • 消费者线程:即线程池中的工作线程,循环从队列中取出任务并执行。

工作流程:

  1. 初始化时,创建固定数量的工作线程,所有线程初始状态为阻塞(等待任务)。
  2. 当生产者提交任务时,将任务加入队列,并唤醒一个等待的工作线程。
  3. 工作线程被唤醒后,从队列中取出任务执行,执行完毕后再次尝试取任务:
    • 若队列非空,继续取任务执行;
    • 若队列为空,线程进入阻塞状态,等待新任务。
  4. 当线程池销毁时,设置退出标志,并唤醒所有线程,让它们处理完当前任务后退出。

1. 应用场景

线程池广泛应用于以下场景:

  • IO密集型操作:如网络请求、磁盘读写,通过线程池并发处理,避免阻塞主线程。
  • 任务数量多且耗时:例如Web服务器处理HTTP请求,每个请求独立处理,使用线程池可显著提升吞吐量。

典型例子:

  • 单Reactor网络模型(如Redis)中,如果某个连接的数据读写耗时较长,会导致其他连接被阻塞。解决方案是将IO操作封装为任务提交给线程池,实现并行处理。
  • Nginx、Memcached等高性能服务器均使用线程池处理IO事件。
  • Workflow、Scanner等开源框架中,线程池是异步任务调度的核心组件。

四、手写线程池(C语言实现)

为了深入理解线程池原理,我们将用C语言实现一个简单的线程池,避免依赖C++的高级特性,专注于核心机制。

1. 接口设计

用户只需要知道三个核心接口:

// thread_pool.h
typedef struct thread_pool thread_pool_t;

// 创建线程池,指定工作线程数量
thread_pool_t* thread_pool_create(int thread_count);

// 提交任务:函数指针 + 上下文参数
int thread_pool_post(thread_pool_t* pool, void (*func)(void*), void* arg);

// 销毁线程池
void thread_pool_destroy(thread_pool_t* pool);

设计原则:

  • 隐藏内部细节:用户无需知道任务队列、线程管理等内部结构。
  • 上下文传递:任务可能需要访问生产者线程的局部数据,通过 void* arg 传递,由调用者负责内存管理(通常使用堆内存,任务执行完毕后释放)。

2. 数据结构设计

2.1 任务节点

每个任务被封装成一个节点,节点中包含函数指针和参数,以及一个指向下一个节点的指针(用于构建队列)。

typedef struct task {
    struct task* next;      // 必须作为第一个成员,便于通用队列操作
    void (*func)(void*);
    void* arg;
} task_t;

注意:next 指针必须是第一个成员,这样可以通过强制类型转换将任意任务结构视为链表节点,实现通用队列。

2.2 任务队列

队列结构包含头指针、尾指针、锁和条件变量,以及一个阻塞标志(用于控制非阻塞模式)。

typedef struct task_queue {
    task_t* head;           // 队列头
    task_t** tail;          // 指向尾节点的next指针的地址(二维指针技巧)
    pthread_spinlock_t spinlock;   // 保护队列操作的自旋锁
    pthread_mutex_t mutex;         // 与条件变量配合使用的互斥锁
    pthread_cond_t cond;           // 条件变量,用于线程阻塞/唤醒
    int block;                     // 阻塞模式标志(1表示阻塞,0表示非阻塞)
} task_queue_t;

二维指针 tail 的巧妙之处:

  • head 指向第一个任务节点。
  • tail 指向最后一个节点的 next 指针的地址(即 &(last_node->next))。
  • 初始时队列为空,head = NULL,tail = &head。
  • 入队时,通过 *tail = new_node 将新节点链接到队尾,然后更新 tail = &(new_node->next)。
  • 这种方式无需区分队列是否为空,代码简洁且高效。

2.3 线程池主体

struct thread_pool {
    task_queue_t* queue;            // 任务队列
    pthread_t* threads;             // 线程ID数组
    int thread_count;               // 线程数量
    atomic_int quit;                 // 退出标志(原子操作)
};

3. 队列操作实现

3.1 创建队列(回滚式编程)

task_queue_t* task_queue_create() {
    task_queue_t* q = malloc(sizeof(task_queue_t));
    if (!q) return NULL;

    q->head = NULL;
    q->tail = &q->head;          // 初始tail指向head的地址
    q->block = 1;                 // 默认阻塞模式

    if (pthread_spin_init(&q->spinlock, 0) != 0) {
        free(q);
        return NULL;
    }
    if (pthread_mutex_init(&q->mutex, NULL) != 0) {
        pthread_spin_destroy(&q->spinlock);
        free(q);
        return NULL;
    }
    if (pthread_cond_init(&q->cond, NULL) != 0) {
        pthread_mutex_destroy(&q->mutex);
        pthread_spin_destroy(&q->spinlock);
        free(q);
        return NULL;
    }
    return q;
}

回滚式编程:每一步初始化失败时,必须释放之前已成功分配的资源,避免内存泄漏。

3.2 入队操作

void task_queue_push(task_queue_t* q, task_t* task) {
    task->next = NULL;           // 新节点的next置空

    pthread_spin_lock(&q->spinlock);
    *q->tail = task;             // 链接到队尾
    q->tail = &task->next;       // 更新tail指向新节点的next
    pthread_spin_unlock(&q->spinlock);

    // 唤醒可能阻塞的消费者线程
    pthread_mutex_lock(&q->mutex);
    pthread_cond_signal(&q->cond);
    pthread_mutex_unlock(&q->mutex);
}

3.3 出队操作(非阻塞)

task_t* task_queue_pop(task_queue_t* q) {
    pthread_spin_lock(&q->spinlock);
    task_t* task = q->head;
    if (task) {
        q->head = task->next;
        if (q->head == NULL)    // 队列变空,重置tail
            q->tail = &q->head;
    }
    pthread_spin_unlock(&q->spinlock);
    return task;
}

3.4 阻塞式获取任务(用于消费者线程)

task_t* task_queue_get(task_queue_t* q) {
    task_t* task;
    while (1) {
        task = task_queue_pop(q);
        if (task) return task;

        // 队列为空,根据模式决定是否阻塞
        if (!q->block) return NULL;   // 非阻塞模式立即返回

        pthread_mutex_lock(&q->mutex);
        // 使用while循环防止虚假唤醒
        while (q->head == NULL && q->block) {
            pthread_cond_wait(&q->cond, &q->mutex);
        }
        pthread_mutex_unlock(&q->mutex);
    }
}

注意:条件变量等待必须放在 while 循环中,因为可能存在虚假唤醒(即使没有信号,也可能返回)。

4. 线程池工作函数

static void* worker_routine(void* arg) {
    thread_pool_t* pool = (thread_pool_t*)arg;
    while (1) {
        // 检查退出标志
        if (atomic_load(&pool->quit)) break;

        task_t* task = task_queue_get(pool->queue);
        if (task) {
            task->func(task->arg);   // 执行任务
            free(task);               // 释放任务内存
        }
    }
    return NULL;
}

5. 线程池创建与销毁

5.1 创建线程池

thread_pool_t* thread_pool_create(int thread_count) {
    thread_pool_t* pool = malloc(sizeof(thread_pool_t));
    if (!pool) return NULL;

    pool->queue = task_queue_create();
    if (!pool->queue) {
        free(pool);
        return NULL;
    }

    atomic_init(&pool->quit, 0);
    pool->thread_count = thread_count;
    pool->threads = malloc(sizeof(pthread_t) * thread_count);
    if (!pool->threads) {
        task_queue_destroy(pool->queue);   // 需要实现销毁函数
        free(pool);
        return NULL;
    }

    for (int i = 0; i < thread_count; ++i) {
        if (pthread_create(&pool->threads[i], NULL, worker_routine, pool) != 0) {
            // 创建失败,销毁已创建的线程和资源
            atomic_store(&pool->quit, 1);   // 让已创建线程退出
            for (int j = 0; j < i; ++j) {
                pthread_join(pool->threads[j], NULL);
            }
            free(pool->threads);
            task_queue_destroy(pool->queue);
            free(pool);
            return NULL;
        }
    }
    return pool;
}

5.2 提交任务

int thread_pool_post(thread_pool_t* pool, void (*func)(void*), void* arg) {
    if (atomic_load(&pool->quit)) return -1;   // 线程池已关闭

    task_t* task = malloc(sizeof(task_t));
    if (!task) return -1;

    task->func = func;
    task->arg = arg;

    task_queue_push(pool->queue, task);
    return 0;
}

5.3 销毁线程池

void thread_pool_destroy(thread_pool_t* pool) {
    if (!pool) return;

    atomic_store(&pool->quit, 1);   // 设置退出标志

    // 唤醒所有等待的线程
    pthread_mutex_lock(&pool->queue->mutex);
    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_queue_destroy(pool->queue);
    free(pool->threads);
    free(pool);
}

6. 队列销毁函数

void task_queue_destroy(task_queue_t* q) {
    // 清空剩余任务
    task_t* task = task_queue_pop(q);
    while (task) {
        free(task);
        task = task_queue_pop(q);
    }

    pthread_spin_destroy(&q->spinlock);
    pthread_mutex_destroy(&q->mutex);
    pthread_cond_destroy(&q->cond);
    free(q);
}

五、线程池的测试与优化

1. 单元测试(使用Google Test)

由于我们的队列是线程池的核心,可以单独测试其逻辑:

#include <gtest/gtest.h>
extern "C" {
    #include "task_queue.h"
}

TEST(TaskQueueTest, PushPop) {
    task_queue_t* q = task_queue_create();
    task_t t1, t2;
    t1.next = NULL;
    t2.next = NULL;

    task_queue_push(q, &t1);
    task_queue_push(q, &t2);

    EXPECT_EQ(task_queue_pop(q), &t1);
    EXPECT_EQ(task_queue_pop(q), &t2);
    EXPECT_EQ(task_queue_pop(q), nullptr);

    task_queue_destroy(q);
}

编译时需链接pthread和gtest。

2. 线程池功能测试

编写一个简单程序测试线程池:

#include "thread_pool.h"
#include <stdio.h>
#include <unistd.h>

void task_func(void* arg) {
    int* num = (int*)arg;
    printf("Task %d is running\n", *num);
    sleep(1);  // 模拟耗时操作
    printf("Task %d finished\n", *num);
    free(arg);
}

int main() {
    thread_pool_t* pool = thread_pool_create(4);
    for (int i = 0; i < 10; ++i) {
        int* arg = malloc(sizeof(int));
        *arg = i;
        thread_pool_post(pool, task_func, arg);
    }
    sleep(3);  // 等待任务完成
    thread_pool_destroy(pool);
    return 0;
}

编译时需要指定动态库路径:

gcc -o test test.c -L. -lthreadpool -lpthread -Wl,-rpath='$ORIGIN'

3. 性能优化考虑

  • 锁的选择:自旋锁适用于短临界区,但如果线程被抢占且持有锁,其他等待线程会忙等,浪费CPU。可以改用互斥锁,或者结合两者(如先自旋几次再睡眠)。
  • 任务队列大小:如果任务产生速度远大于消费速度,队列会无限增长,导致内存压力。可设置最大队列长度,当队列满时,生产者可选择阻塞或返回错误。
  • 工作线程数量:通过压测找到最佳线程数,考虑任务类型、CPU核心数、IO等待时间等因素。
posted @ 2026-03-18 17:00  xggx  阅读(37)  评论(0)    收藏  举报