原来手撕线程池这么简单!

1. 简介与背景

什么是线程池

线程池是一种用于多线程编程的资源管理结构。它通过预先创建一组固定或动态数量的线程,并将其用于处理任务,避免在每个任务执行前都创建线程、任务结束后再销毁线程的开销。线程池能够高效管理线程的创建、调度和销毁,使得任务的并发执行更加高效。

线程池的重要性

  1. 减少线程创建/销毁开销:每次创建或销毁线程都会有一定的系统资源开销。线程池通过复用线程减少了这一开销,提高了整体性能。

  2. 控制系统资源:线程数量过多会导致系统资源枯竭。线程池通过固定数量的工作线程避免了创建过多线程导致的资源枯竭问题。

  3. 合理控制并发:通过合理分配和调度线程,线程池可以控制任务的并发执行,防止系统资源被大量并发任务耗尽。

  4. 统一线程管理:通过集中化的管理,线程池可以统一监控和优化线程的调度和执行,提高了程序的可维护性和灵活性。

2. 设计目标与结构概览

设计目标

  1. 可伸缩性:线程池应能够根据实际工作负载灵活配置线程数,使其具有可伸缩性。

  2. 任务队列:线程池需要使用一个安全、高效的任务队列,以确保多个工作线程能够并发、安全地添加和获取任务。

  3. 线程管理:线程池应当具备完善的线程管理机制,能够监控线程的健康状况并及时处理异常或故障。

  4. 可扩展性:为未来扩展功能、添加特性预留空间,例如优先级调度、动态调整线程数等。

代码结构

  1. 任务结构体 (task_t):封装了每个任务的信息,包括待执行的函数指针 func 和参数 arg,并通过 next 指针链接形成链表,作为任务队列的节点。
typedef struct task_s {
    void *next; //链接下一个任务
    handler_pt func; 
    void *arg; //在堆上进行维护
} task_t;
  1. 任务队列结构体 (task_queue_t)
    • head:指向任务队列的首个任务节点。
    • tail:双指针,始终指向队列最后一个任务的 next 指针,以便快速添加新任务。此处也可以用单指针,但是用双指针会有很多好处。留个彩蛋,最后解释!
    • block:表示队列的阻塞状态,决定线程在无任务可执行时是否阻塞等待。
    • 锁与条件变量:使用 spinlockpthread_mutex/pthread_cond 确保多线程访问任务队列时的安全性。
typedef struct task_queue_s {
    void *head;
    void **tail;   
    int block;  // 1为阻塞;0为非阻塞
    spinlock_t lock;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
} task_queue_t;
  1. 线程池结构体 (threadpool_t)
    • task_queue:指向任务队列。
    • quit:原子标志位,指示线程池是否要停止工作。
    • thrd_count:已创建的工作线程数量。
    • threads:存储所有线程的数组,方便对线程进行管理。
struct threadpool_s {
    task_queue_t *task_queue;
    atomic_int quit; //原子变量
    int thrd_count;
    pthread_t *threads;
};

3. 实现细节逐一讲解

任务结构体和队列

  1. 任务结构体 task_t

    • 每个任务由一个结构体 task_t 表示,结构体包含:
      • func:任务处理函数的指针。
      • arg:任务处理函数的参数。
      • next:指向下一个任务的指针,以形成链表结构。
    • 新任务在 thrdpool_post 函数中被创建,并由 __add_task 函数添加到任务队列。
  2. 任务队列结构体 task_queue_t

    • headtail 形成链表,用于存储提交的任务。
    • tail 是一个双指针,指向最后一个任务的 next 指针,方便快速追加任务。
    • lockmutex/cond 用于同步,确保多线程访问任务队列的安全性。
  3. 队列操作函数

  • __taskqueue_create:分配内存并初始化任务队列的锁、条件变量和双指针。

  • __add_task

    • 使用自旋锁 (spinlock) 保证多线程访问时的安全性。
    • 通过双指针快速更新 tail,将新任务追加到队列的末尾。
  • __pop_task

    • 从队列头部移除任务,更新 headtail 指针。
  • __get_task

    • 调用 __pop_task 尝试获取任务。
    • 若队列为空,使用条件变量等待直到有新任务添加或退出信号发出。

线程池工作函数

  1. __thrdpool_worker
    • 每个工作线程运行的主函数。
    • 持续运行一个 while 循环,直到 quit 原子标志被设置为 1。
    • 通过 __get_task 获取任务,获取不到时进入等待状态。
    • 执行获取到的任务函数,并在完成后释放任务。

线程管理

  1. __threads_create

    • 使用 pthread_attr_init 初始化线程属性。
    • 为线程数组分配内存,逐个调用 pthread_create 创建线程并指向 __thrdpool_worker 作为线程入口。
    • 如果有线程创建失败,调用 __threads_terminate 清理已创建的线程。
  2. __threads_terminate

    • 设置 quit 标志位,表示线程池应停止工作。
    • 调用 __nonblock 唤醒所有正在等待任务的线程。
    • 使用 pthread_join 等待所有线程结束。

线程池的创建和任务分发

  1. threadpool_create

    • 初始化线程池结构体并分配内存。
    • 使用 __taskqueue_create 创建任务队列。
    • 初始化 quit 原子标志,并使用 __threads_create 创建线程。
  2. thrdpool_post

    • 创建新任务,将任务处理函数和参数存储在 task_t 结构体中。
    • 通过 __add_task 将新任务追加到任务队列。
  3. thrdpool_waitdone

    • 调用 pthread_join 等待线程池中所有工作线程完成任务。
    • 使用 __taskqueue_destroy 销毁任务队列,释放分配给线程池的内存。

彩蛋

tail 作为双指针 (void **) 而不是单指针,有以下好处:

  1. 保持尾部节点的直接引用:双指针允许直接操作链表尾部的指针,以便快速更新。对于添加新任务(节点)的操作,tail 始终指向链表尾部节点的 next 指针位置。这意味着,当添加新任务时,双指针直接指向链表最后一个节点的 next 指针,因此新节点可以被立即链接到链表的尾部。

  2. 简化链表操作:由于双指针直接指向尾部节点的 next 指针,添加节点的操作不需要遍历整个链表或查找尾部节点,可以直接将新任务链接到队列的尾部,同时更新 tail 指向新的尾部节点的 next 指针。这使得添加节点操作时间复杂度保持为 O(1),而不是 O(n)。

  3. 保持队列一致性:当队列为空时,双指针 tail 指向 head 指针,这意味着在 head 为空的情况下,tail 也能够正确更新,始终保持队列尾部引用的准确性。

  4. 方便移植:不限定任务类型,只要任务的结构体起始内存是一个用于链接的指针,都能插入到任务队列中,仔细查看代码,你会发现,代码中并没有 显式地 用到 next 指针 ,而是用 tail双指针替代 next 指针。如果不太理解 可以看看以下代码,相信你会恍然大悟的。

单指针的局限性:

如果使用单指针:

  • 需要存储队列尾部节点的指针,并且在添加节点时,先遍历到尾部节点才能进行插入操作,这会使添加节点的时间复杂度增加为 O(n)。
  • 如果队列为空,则需要额外逻辑来更新空队列的状态,以确保在插入第一个节点后能够正确维护 headtail 之间的关系。

4. 完整代码

头文件

#ifndef _THREAD_POOL_H
#define _THREAD_POOL_H

typedef struct threadpool_s threadpool_t;
// 任务执行的规范 ctx 上下文
typedef void (*handler_pt)(void * /* ctx */);
#ifdef __cplusplus
extern "C"
{
#endif
// 对称处理
threadpool_t *threadpool_create(int thrd_count);
void threadpool_terminate(threadpool_t * pool);
//抛任务
int threadpool_post(threadpool_t *pool, handler_pt func, void *arg);
void threadpool_waitdone(threadpool_t *pool);
#ifdef __cplusplus
}
#endif
#endif

源文件

#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdlib.h>
#include "thread_pool.h"
#include "spinlock.h"

typedef struct spinlock spinlock_t;
typedef void (*handler_pt)(void * /* ctx */);

typedef struct task_s {
    void *next; //链接下一个任务
    handler_pt func; 
    void *arg; //在堆上进行维护
} task_t;

typedef struct task_queue_s {
    void *head;
    void **tail;   //指向*next,起始
    /*
    例:
        task_t ptask
        task_t *ptail=&ptask;
        task_t **tail=&ptask;
        ptail tail 起始位置一样都指向ptask
        但是ptail的内存范围为24个字节,
        tail 是指向指针的指针,内存范围为ptask中的*next,也就是8个字节
    */
    int block;  // 1为阻塞;0为非阻塞
    spinlock_t lock;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
} task_queue_t;

struct threadpool_s {
    task_queue_t *task_queue;
    atomic_int quit; //原子变量
    int thrd_count;
    pthread_t *threads;
};
//资源的创建  回滚式编程
//业务逻辑    防御式编程
//------------------------消息队列-----------------
static task_queue_t * __taskqueue_create(){
    int ret;
    task_queue_t *queue=(task_queue_t *)malloc(sizeof(task_queue_t));
    if(queue){
        //成功情况下
        ret=pthread_mutex_init(&queue->mutex,NULL);
        if(ret==0){
            ret=pthread_cond_init(&queue->cond,NULL);
            if(ret==0){
                spinlock_init(&queue->lock);
                queue->head=NULL;
                queue->tail=&queue->head;
                queue->block=1;
                return queue;
            }
            pthread_mutex_destroy(&queue->mutex);
        }
        free(queue);
    }
    //分配失败
    return NULL;
}

static void __nonblock(task_queue_t *queue){
    pthread_mutex_lock(&queue->mutex);
    queue->block=0;
    pthread_mutex_unlock(&queue->mutex);
    pthread_cond_broadcast(&queue->cond);//广播 让所有在条件变量休眠的线程进行唤醒
}

/*   
        inline:
        减少函数调用的开销:每次调用函数时,程序都需要执行一系列的操作,包括保存寄存器、跳转、参数传递等。
            这些操作可能会带来额外的性能开销。使用 inline 将函数的代码直接嵌入到调用点,减少了这些额外操作。
        提高性能:对于小型、频繁调用的函数,inline 能够通过直接嵌入代码来提高性能。
            避免了函数调用的开销,有时还可以让编译器更好地优化代码。
        inline 修饰,意在让编译器尽可能在调用点直接插入代码,以减少调用任务队列函数的开销。
        inline 只是一个编译器建议,而非强制要求。编译器可能会根据优化策略决定是否实际内联该函数
        tail 采用双指针:
        不限定任务类型,只要该任务的结构起始内存是一个用于链接的指针
        保持尾部节点的直接引用:双指针允许直接操作链表尾部的指针,以便快速更新。
            对于添加新任务(节点)的操作,tail 始终指向链表尾部节点的 next 指针位置。
            这意味着,当添加新任务时,双指针直接指向链表最后一个节点的 next 指针,
            因此新节点可以被立即链接到链表的尾部。
        简化链表操作:由于双指针直接指向尾部节点的 next 指针,
            添加节点的操作不需要遍历整个链表或查找尾部节点,可以直接将新任务链接到队列的尾部,
            同时更新 tail 指向新的尾部节点的 next 指针。这使得添加节点操作时间复杂度保持为 O(1),
            而不是 O(n)。
        保持队列一致性:当队列为空时,双指针 tail 指向 head 指针,
            这意味着在 head 为空的情况下,tail 也能够正确更新,始终保持队列尾部引用的准确性。
   */ 
static inline void 
__add_task(task_queue_t *queue,task_t *task){
    void **link=(void**)task; //强转成二维指针,起始地址没有发生编号,但是内存范围变为8个字节,link就指向了next指针
    *link=NULL; //新加的task->next =NULL;
    spinlock_lock(&queue->lock);
    //对**tail解引用得到*next,让 *next指向link
    *queue->tail/* 等价于 queue->tail  ->next  */ = link;
    queue->tail=link; // tail 是个二级指针 指向*next ,队列queue->tail 指向 新增加的next 
    spinlock_unlock(&queue->lock);
    pthread_cond_signal(&queue->cond);
}

static inline void * 
__pop_task(task_queue_t *queue) {
    spinlock_lock(&queue->lock);
    if (queue->head == NULL) {
        spinlock_unlock(&queue->lock);
        return NULL;
    }
    task_t *task;
    task = queue->head;

    void **link = (void**)task;
    queue->head = *link;

    if (queue->head == NULL) {
        queue->tail = &queue->head;
    }
    spinlock_unlock(&queue->lock);
    return task;
}

static inline void * 
__get_task(task_queue_t *queue) {
    task_t *task;
    // 虚假唤醒
    while ((task = __pop_task(queue)) == NULL) {
        pthread_mutex_lock(&queue->mutex);
        if (queue->block == 0) {
            pthread_mutex_unlock(&queue->mutex);
            return NULL;
        }
        // 1. 先 unlock(&mtx)
        // 2. 在 cond 休眠
        // --- __add_task 时唤醒
        // 3. 在 cond 唤醒
        // 4. 加上 lock(&mtx);
        pthread_cond_wait(&queue->cond, &queue->mutex);
        pthread_mutex_unlock(&queue->mutex);
    }
    return task;
}
static void
__taskqueue_destroy(task_queue_t *queue) {
    task_t *task;
    while ((task = __pop_task(queue))) {
        free(task);
    }
    spinlock_destroy(&queue->lock);
    pthread_cond_destroy(&queue->cond);
    pthread_mutex_destroy(&queue->mutex);
    free(queue);
}


//-------->线程池<--------------

static void *
__thrdpool_worker(void *arg) {
    threadpool_t *pool = (threadpool_t*) arg;
    task_t *task;
    void *ctx;

    while (atomic_load(&pool->quit) == 0) {
        task = (task_t*)__get_task(pool->task_queue);
        if (!task) break;
        handler_pt func = task->func;
        ctx = task->arg;
        free(task);
        func(ctx);
    }
    
    return NULL;
}

static void 
__threads_terminate(threadpool_t * pool) {
    atomic_store(&pool->quit, 1);
    __nonblock(pool->task_queue);
    int i;
    for (i=0; i<pool->thrd_count; i++) {
        pthread_join(pool->threads[i], NULL);
    }
}

static int 
__threads_create(threadpool_t *pool, size_t thrd_count) {
    pthread_attr_t attr;
	int ret;

    ret = pthread_attr_init(&attr);

    if (ret == 0) {
        pool->threads = (pthread_t *)malloc(sizeof(pthread_t) * thrd_count);
        if (pool->threads) {
            int i = 0;
            for (; i < thrd_count; i++) {
                if (pthread_create(&pool->threads[i], &attr, __thrdpool_worker, pool) != 0) {
                    break;
                }
            }
            pool->thrd_count = i;
            pthread_attr_destroy(&attr);
            if (i == thrd_count)
                return 0;
            __threads_terminate(pool);
            free(pool->threads);
        }
        ret = -1;
    }
    return ret; 
}
//---------------------对外接口实现------------------------------
void
threadpool_terminate(threadpool_t * pool) {
    atomic_store(&pool->quit, 1);
    __nonblock(pool->task_queue);
}


threadpool_t * threadpool_create(int thread_count){
    threadpool_t *pool;
    pool=(threadpool_t *)malloc(sizeof(*pool));//sizeof是个运算符,在编译器进行替换
    if(pool){//创建成功
        task_queue_t*queue=__taskqueue_create();
        if(queue){
            pool->task_queue=queue;
            atomic_init(&pool->quit, 0);
            if(__threads_create(pool,thread_count)==0){
                return pool;  
            }
            __taskqueue_destroy(queue);
        }
        free(pool);
    }
    return NULL;
}

int
thrdpool_post(threadpool_t *pool, handler_pt func, void *arg) {
    if (atomic_load(&pool->quit) == 1) 
        return -1;
    task_t *task = (task_t*) malloc(sizeof(task_t));
    if (!task) return -1;
    task->func = func;
    task->arg = arg;
    __add_task(pool->task_queue, task);
    return 0;
}

void
thrdpool_waitdone(threadpool_t *pool) {
    int i;
    for (i=0; i<pool->thrd_count; i++) {
        pthread_join(pool->threads[i], NULL);
    }
    __taskqueue_destroy(pool->task_queue);
    free(pool->threads);
    free(pool);
}


  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值