基础组件(一):线程池


一、线程池的原理和实现

1. 线程池是什么?

一种维持管理固定数量线程的的池式结构。
(其他池式结构:数据库连接池、内存池、对象池, 共同点:复用资源)

线程池预先创建一定数量的线程,当有任务提交时,线程池会从空闲线程中选取一个来执行该任务;若没有空闲线程,任务会被放入任务队列等待;当线程完成任务后,不会被销毁,而是继续等待下一个任务,这样可以避免频繁创建和销毁线程带来的开销。

线程池的作用 (为什么需要线程池)

某类任务特别耗时,严重影响该线程处理其他任务,因此想到把这类任务放在其他线程异步执行。

线程池的作用:
1. 复用线程资源,减少线程创建和销毁的开销
2. 可异步执行耗时任务,性能优化,不过度占用核心线程
3. 并发执行核心业务,充分利用多核资源,减少了多任务的执行时间

线程池的构成(生产者-任务队列-消费者线程)

线程池属于一种生产消费模型,线程池运行环境构成:
1. 生产者线程:发布任务
2. 任务队列:存储任务,调度线程池
3. 消费者线程:取出任务,执行任务

在这里插入图片描述


2. 为什么维持管理固定数量的线程池?

维持固定数量的线程池作用

1.由于系统资源的限制,随着线程数量继续增加,不再带来性能的提升反而带来负担。
2.避免线程频繁的创建和销毁,消耗资源。

线程池中线程数量如何确定(经验公式:CPU核数)

依据 “ 充分利用系统资源 ”
CPU密集型 : CPU核心数
IO密集型(网络IO,磁盘IO):2 * CPU核心数
=> (线程等待时间 + cpu运算时间)* cpu 核心数 / cpu运算时间


3*. 线程池怎么解决问题的? (拓展)

  1. 初始化阶段
    配置参数:开发者需要根据系统的资源状况、任务特性等因素,为线程池配置关键参数。比如最大线程数,它限定了线程池同时能运行的线程上限,避免系统因创建过多线程而耗尽资源;最小线程数规定了线程池初始创建并保持的线程数量;任务队列大小则决定了在所有线程都忙碌时,可暂存任务的数量。

    创建线程:依据配置好的最小线程数,线程池开始预先创建相应数量的线程。这些线程在创建后处于就绪状态,随时等待执行任务,这一操作减少了后续任务到来时的线程创建时间,提升了系统响应速度。

  2. 任务提交阶段
    接收任务:当有新任务提交到线程池时,线程池会对任务进行接收和管理。任务可以是各种类型的计算任务、I/O 操作等。

    分配任务:线程池会检查当前是否有空闲线程。若存在空闲线程,就会立即将任务分配给其中一个空闲线程执行;若所有线程都在忙碌,任务会被放入任务队列中等待。

  3. 任务执行阶段
    线程执行任务:获得任务的线程开始执行具体的任务逻辑。在执行过程中,线程会利用系统资源完成任务,由于线程是预先创建好的,避免了频繁创建和销毁线程带来的开销。

    异常处理:若线程在执行任务时抛出异常,线程池会捕获该异常并进行相应处理。通常会将异常信息记录下来,同时将该线程标记为空闲状态,以便后续继续执行其他任务,保证系统的稳定性。

  4. 任务队列管理阶段
    任务排队:当任务队列中有任务等待,且线程池中有空闲线程时,线程池会从任务队列中取出一个任务分配给空闲线程执行,确保任务按照一定的顺序得到处理。

    队列满处理:若任务队列已满,且所有线程都在忙碌,此时新提交的任务可能会根据线程池的配置策略进行处理,比如拒绝执行任务并抛出异常,或者等待队列中有任务完成后再加入队列。

  5. 线程管理阶段
    线程复用:线程完成任务后,不会被销毁,而是返回到线程池的空闲线程列表中,等待下一个任务的分配。这样可以实现线程的复用,减少系统资源的消耗。

    线程调整:线程池会根据系统的负载情况和任务的执行情况,动态调整线程的数量。例如,当任务量突然增加,且任务队列中的任务数量达到一定阈值时,线程池可能会创建新的线程来处理任务;当任务量减少,空闲线程过多时,线程池可能会销毁一些多余的线程,以节省系统资源。

  6. 关闭阶段
    停止接收新任务:当系统不再需要线程池处理任务时,会通知线程池停止接收新的任务。
    完成剩余任务:线程池会继续执行任务队列中剩余的任务,直到所有任务都执行完毕。
    销毁线程:在所有任务执行完成后,线程池会销毁所有线程,释放系统资源。


4. 手撕线程池

* 封装原则 (隐藏实现细节,暴露使用接口)

  • 隐藏实现细节,暴露使用接口, 客户知道的越少越好。
用户是否需要知道线程池结构
用户是否需要知道队列
用户是否需要知道任务以何种形式执行
用户是否需要知道任务结构
用户是否需要知道线程管理的细节
  • 编码对称接口设计
    资源创建时,回滚式编程
    流程处理时,防御式编程

1. 数据结构设计

  • 任务结构

    typedef struct task_s {
         
         
        void *next; //指针指向下一个任务
        //任务的处理函数指针,当任务被线程执行时,会调用这个函数
        handler_pt func; 
        void *arg; //函数参数(上下文)
    } task_t;
    
  • 阻塞队列 (任务队列)

    typedef struct task_queue_s {
         
         
        void *head;
        void **tail; 
        int block;//阻塞/非阻塞
        // 加锁实现对临界资源访问的保护
        spinlock_t lock;//自旋锁
        pthread_mutex_t mutex;//互斥锁
        pthread_cond_t cond;//信号量
    } task_queue_t;
    
    ==============================================
    + block:
    表示队列是否阻塞。为 1 时,队列处于阻塞状态,等待任务到来;为 0 时,不阻塞,即用于线程退出时唤醒所有等待线程。
    + spinlock_t lock:
    自旋锁,用于保护队列的链表操作,保证对head和tail的并发访问安全。
    + pthread_mutex_t mutex; 与 pthread_cond_t cond:
    互斥锁与条件变量用于实现等待和唤醒机制。当队列中没有任务时,工作线程会进入等待状态;有新任务时,通过条件变量唤醒等待线程。
    
  • 线程池的结构

    struct thrdpool_s {
         
         
        task_queue_t *task_queue; //任务队列
        atomic_int quit; //原子变量
        int thrd_count; //线程数量
        pthread_t *threads; //线程数组
    };
    
    ==============================================
    + atomic_int quit:
    线程池是否退出的标志,通过原子操作保证在多线程环境下的安全性。当设置为 1 时,表示线程池正在退出或已经退出。
    + thrd_count:
    表示线程池中成功创建并运行的线程数量。
    + pthread_t *threads:用于存储线程 ID 的数组,用于后续 join 等操作。
    

2. 接口设计

在这里插入图片描述

1. thrdpool_create(int thrd_count)
创建并初始化一个线程池对象。
它接受一个参数 thrd_count,表示线程池中要创建的线程数量。
内部会创建一个任务队列,用于存储待执行的任务,并初始化线程池中的线程。
线程池的线程会等待并执行队列中的任务。


// 创建并初始化一个线程池对象
thrdpool_t *thrdpool_create(int thrd_count) {
   
   
    thrdpool_t *pool;
 
    pool = (thrdpool_t*)malloc(sizeof(*pool));  // 分配线程池结构体
    if (pool) {
   
   
        task_queue_t *queue = __taskqueue_create();  // 创建任务队列
        if (queue) {
   
   
            pool->task_queue = queue;
            atomic_init(&pool->quit, 0);  // 初始化退出标志
            if (__threads_create(pool, thrd_count) == 0)  // 创建线程
                return pool;
            __taskqueue_destroy(queue);  // 任务队列创建失败时销毁
        }
        free(pool);  // 线程池创建失败时释放内存
    }
    return NULL;  // 返回NULL表示创建失败
}

========================================================
2.thrdpool_post(thrdpool_t *pool, handler_pt func, void *arg)
向线程池提交一个任务,任务由指定的处理函数 func 和任务参数 arg 组成。
该函数会将任务添加到线程池的任务队列中。

// 向线程池提交任务,将任务添加到任务队列中
int thrdpool_post(thrdpool_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;  // 成功返回0
}

========================================================
3.thrdpool_terminate(thrdpool_t *pool)
终止线程池,停止所有线程的执行.它会设置 quit 标志为 1,并且使任务队列变为非阻塞状态,从而通知所有工作线程退出。

// 终止线程池,停止所有线程的执行
void thrdpool_terminate(thrdpool_t *pool) {
   
   
    atomic_store(&pool->quit, 1);  // 设置退出标志,通知线程池关闭
    __nonblock(pool->task_queue);  // 设置任务队列为非阻塞模式,允许线程退出
}

============================================
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值