01 为什么需要线程池

首要明白两个地方:

  1. 线程的创建需要内存资源
  2. 线程的创建和销毁需要时间资源

显而易见,由于以上两个原因,不得不寻找一个折衷的方式面对多任务的问题。如果我们只创建一定量的线程,且在一个线程执行完某一任务后,重复利用该线程去处理新的任务而不是直接销毁它,那么通过这两个策略我们就可以“朴素”的解决以上两个问题。

线程池的初衷就是想搭建一个有一定数量线程,且可以重复利用这些线程处理若干任务的小环境。

02 线程池Run起来是个什么样子

俗话说:能不能行?Run一下再说。

对于线程池来说,主要由以下三组件组成:

  • 线程队列
    • 用途:用来存放被创建的线程
    • 这些线程主要处于两种状态
      • 正在执行任务(运行)
      • 正在等待分配任务(阻塞)
  • 任务队列
    • 用途:将新任务添加到队列最后,并通知空闲线程可以从队列最前端取用任务执行
  • 控制器
    • 管理着一个队列锁和一个信号量
      • 队列锁——因为多个线程对同一任务队列进行任务取用的时候,会有数据竞争(Data Race),所以对任务队列进行存、取操作的时候都需要加锁,处理完后解锁。
      • 信号量——在任务队列有新任务的时候,一旦启用信号量,某一处于阻塞的线程同时获取队列锁和信号量从而解阻塞->取用任务->执行
    • 三个主要的方法
      • 添加任务到任务队列——add_task()
      • 通知线程有新任务——_run_task()
      • 销毁线程池开辟的所有资源并结束该批次多任务处理——destory_pool()

03 重要模块介绍

互斥锁和信号量

// 创建锁和信号量
pthread_mutex_t queue_lock;
pthread_cond_t	queue_ready;
// 初始化锁和信号量
pthread_mutex_init(&(queue_lock), NULL);
pthread_cond_init(&(queue_ready), NULL);
// 销毁锁和信号量
pthread_mutex_destroy(&(queue_lock));
pthread_cond_destroy(&(queue_ready));

// 尝试加锁以及解锁
pthread_mutex_lock(&(queue_lock));
pthread_mutex_unlock(&(queue_lock));

// 该函数会阻塞线程
// 如果当前线程获得该锁但是没有获得信号量通知--->解锁并阻塞
// 当前线程同时获得锁和信号量--->加锁、消耗本次信号量并解阻塞
pthread_cond_wait(&(queue_ready), &(queue_lock));

// 使信号量可以被某一等待该信号量可用的线程使用
pthread_cond_signal(&(queue_ready));

// 使所有等待信号量可用的线程均可以使用该信号量
// 在该线程池设计中,摧毁线程池的时候调起所有线程用到该方法
pthread_cond_broadcast(&(queue_ready));

04 Show me the code

库函数

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>

线程节点和任务节点

// 线程节点
typedef struct CPTHREAD_NODE {
    struct CPTHREAD_NODE* pre;
    struct CPTHREAD_NODE* next;
    
    // 线程节点中的线程
    pthread_t pthread;
} cpthread_node_t;


// 任务节点
typedef struct CWORK_NODE {
    struct CWORK_NODE* pre;
    struct CWORK_NODE* next;
    
    // 任务节点中的具体任务
    void*(*process)(void* arg);
    // 该任务的参数
    void* arg;
} cwork_node_t;

线程池,其中的函数借口用函数指针的形式来声明,在结构体外定义并在初始化函数中对其初始化。

// 线程池
typedef struct CTHREAD_POOL {
    // 任务队列锁以及信号量
    pthread_mutex_t queue_lock;
    pthread_cond_t	queue_ready;

    int MAX_THREAD_NUM;         // 最大线程数量
    int MAX_FREE_THREAD_NUM;    // 最大空闲线程数量

    int current_thread_num;     // 当前线程总数
    int current_wait_task_num;  // 当前等待的任务数
    int current_task_num;       // 当前正在执行的任务数量
    int current_free_num;       // 当前空闲线程数量

    int shutdown;               // 如果为1则已经销毁

    // 线程队列和任务队列
    cpthread_node_t* pthread_queue;
    cwork_node_t* ptask_queue;


    // 添加一个任务
    int
    (*add_task)
    (void* pthis, void*(*process)(void* arg), void* arg);


    // 等待所有任务完成,释放线程池资源
    int
    (*destory_pool)(void* pthis);


    // 执行任务
    void*
    (*_run_task)
    (void* pthis);
  
} cthread_pool_t;

供内部调用的任务执行函数,这里注意到最后在当前线程退出后,程序中在最后的销毁线程池之前,没有将这一退出的线程节点的内存归还给操作系统,这是因为设计的相对简单,逻辑主要在于线程池的整体运作,可供读者改进。

思路是在控制池结构体的线程队列中,剔除已经退出线程的节点,并且将该节点内存释放。

注意这里虽然没有及时释放,但是最终在调用销毁线程池的函数中,会对所有的线程节点进行退出并释放资源,所以不存在内存泄漏,只是存在一定的内存浪费。当最大允许空闲线程数等于最大允许线程数的时候,就不必担心浪费了,具体逻辑参看代码。

// 内部调用执行任务的函数
void*
_run_task(void* pthis) {
    cthread_pool_t* pool = (cthread_pool_t*) pthis;

    while (1) {
        // 上锁
        pthread_mutex_lock(&(pool->queue_lock));

        // 当任务队列为空,且不准备销毁线程池的时候
        while ((pool->current_wait_task_num == 0) &&
               (pool->shutdown == 0)) {
            // 释放当前锁,并等待信号量的通知
            // 当得到信号量通知,且重新加锁后,将解除阻塞
            // 注意解除阻塞后该线程是获得互斥锁的
            pthread_cond_wait(&(pool->queue_ready), &(pool->queue_lock));
        }

        // 如果要销毁线程池
        if (pool->shutdown) {
            // 解锁
            pthread_mutex_unlock(&(pool->queue_lock));
            // 退出
            pthread_exit(NULL);
        }

        assert(pool->ptask_queue != NULL);
        assert(pool->current_wait_task_num != 0);

        // 获取任务等待队列的第一个任务
        pool->current_wait_task_num -= 1;
        pool->current_task_num += 1;
        pool->current_free_num -= 1;
        cwork_node_t * work = pool->ptask_queue;
        pool->ptask_queue = pool->ptask_queue->next;

        // 开锁
        pthread_mutex_unlock(&(pool->queue_lock));

        // 执行获取到的任务
        (*(work->process))(work->arg);
        free(work);
        work = NULL;  // 防止悬空指针

        // 获取锁
        pthread_mutex_lock(&(pool->queue_lock));
        pool->current_task_num -= 1;
        pool->current_free_num += 1;

        // 如果当前空闲线程大于最大空闲线程,则释放当前线程
        if (pool->current_free_num > pool->MAX_FREE_THREAD_NUM) {
            pool->current_thread_num -= 1;
            pool->current_free_num -= 1;
            pthread_mutex_unlock(&(pool->queue_lock));
            break;
        }

        // 开锁
        pthread_mutex_unlock(&(pool->queue_lock));

    }

    pthread_exit(NULL);
}

添加任务到任务队列

注意,该部分63行和71行的开锁与信号量的顺序,有的博客上看到必须要先解锁再使信号量可用,但是个人认为需要先使信号量可用再进行解锁。考虑如果有多个线程调用添加任务的函数,一旦先解锁那么这时候其他线程的添加任务的函数中,锁又会被获取,而执行任务函数中由于信号量还不可用导致没有能力去竞争锁的使用资格所以,这样一来可能会出现“负载不均衡“的情况。而如果先使得信号量可用后再进行解锁,一旦信号量可用,那么执行函数和任务添加函数同时拥有有获取锁的能力,更加合理?如有不正确的地方,迫切希望您可以评论指正,谢谢。

【参考博客后,证明我的顾虑是有价值的请查看该博客
Linx中的信号等待和锁等待是两个队列,先发信号后解锁不会造成性能损失,而先解锁再发信号,可能会造成丢失cpu,即被低优先级的线程抢占锁。

// 添加一个任务
int
add_task
(void* pthis, void*(*process)(void* arg), void* arg) {
    cthread_pool_t* pool = (cthread_pool_t*) pthis;

    // 创建新的任务节点,并初始化
    cwork_node_t* work = (cwork_node_t*)malloc(sizeof(cwork_node_t));
    work->pre = NULL;
    work->next = NULL;
    work->process = process;
    work->arg = arg;

    // 开始向队列添加任务
    // 获取队列锁
    pthread_mutex_lock(&(pool->queue_lock));

    // 如果任务队列为空
    if (pool->current_wait_task_num == 0) {
        pool->ptask_queue = work;
    } else {  // 不为空则在最后添加任务
        cwork_node_t* cursor = pool->ptask_queue;

        while(cursor->next != NULL) {
            cursor = cursor->next;
        }

        cursor->next = work;
        work->pre = cursor;
    }

    // 如果任务队列仍为空,则不通过
    assert(pool->ptask_queue != NULL);

    // 任务数加1
    pool->current_wait_task_num += 1;

    // 当前没有空线程,且总线程数量没有超过最大线程数量
    if ((pool->current_free_num == 0) &&
        (pool->current_thread_num < pool->MAX_THREAD_NUM)) {
        // 创建新线程节点并初始化
        pool->current_thread_num += 1;
        pool->current_free_num += 1;
        cpthread_node_t* new_thread = (cpthread_node_t*)malloc(sizeof(cpthread_node_t));
        printf("New thread born.\n");
        new_thread->pre = NULL;
        new_thread->next = NULL;
        pthread_create(&(new_thread->pthread), NULL, pool->_run_task, (void*)pool);

        cpthread_node_t* cursor = pool->pthread_queue;

        if (cursor == NULL) {   // 空线程队列
            pool->pthread_queue = new_thread;
        } else {    // 线程队列不为空

            while (cursor->next != NULL) {
                cursor = cursor->next;
            }

            cursor->next = new_thread;
        }

        // 顺序争议处
        pthread_cond_signal(&(pool->queue_ready));
        pthread_mutex_unlock(&(pool->queue_lock));


        return 0;
    }

    // 顺序争议处
    pthread_cond_signal(&(pool->queue_ready));
    pthread_mutex_unlock(&(pool->queue_lock));

    return 0;
}

摧毁线程池并回收资源

// 销毁线程池
int destory_pool(void* pthis) {
    cthread_pool_t* pool = (cthread_pool_t*)pthis;

    if (pool->shutdown == 1) {
        // 重复销毁
        return -1;
    }

    pool->shutdown = 1;

    // 唤醒所有线程
    pthread_cond_broadcast(&(pool->queue_ready));

    // 等待正在运行的所有任务完成,并释放线程队列内存
    while (pool->pthread_queue->next != NULL) {
        pthread_join(pool->pthread_queue->pthread, NULL);
        cpthread_node_t* current_node = pool->pthread_queue;
        pool->pthread_queue = pool->pthread_queue->next;
        free(current_node);
        printf("One thread die.\n");
        current_node = NULL;
    }

    if (pool->pthread_queue != NULL) {
        pthread_join(pool->pthread_queue->pthread, NULL);
        free(pool->pthread_queue);
        printf("One thread die.\n");
        pool->pthread_queue = NULL;
    }

    // 释放任务队列内存
    if (pool->ptask_queue != NULL) {
        while (pool->ptask_queue->next != NULL) {
            cwork_node_t* current = pool->ptask_queue;
            pool->ptask_queue = pool->ptask_queue->next;
            free(current);
            current = NULL;
        }

        if (pool->ptask_queue != NULL) {
            free(pool->ptask_queue);
            pool->ptask_queue = NULL;
        }

    }

    // 销毁锁和信号量
    pthread_mutex_destroy(&(pool->queue_lock));
    pthread_cond_destroy(&(pool->queue_ready));

    // 释放线程池总体内存
    free(pool);
    pool = NULL;

    return 0;
}

线程池初始化

// 初始化一个线程池
cthread_pool_t*
create_pool() {
    // 申请线程池所需要的内存
    cthread_pool_t* pool = (cthread_pool_t*)malloc(sizeof(cthread_pool_t));

    if (pool == NULL) {
        return NULL;
    }

    // 标准操作
    memset(pool, 0, sizeof(cthread_pool_t));

    // 初始化锁和信号量
    pthread_mutex_init(&(pool->queue_lock), NULL);
    pthread_cond_init(&(pool->queue_ready), NULL);

    pool->MAX_THREAD_NUM          = 4;         // 最大线程数量
    pool->MAX_FREE_THREAD_NUM     = 2;         // 最大空闲线程数量

    pool->current_thread_num      = 0;          // 当前线程总数
    pool->current_wait_task_num   = 0;          // 当前等待的任务数
    pool->current_task_num        = 0;          // 当前正在执行的任务数量
    pool->current_free_num        = 0;          // 当前空闲线程数量

    pool->shutdown                = 0;          // 如果为1则已经销毁

    // 线程队列和任务队列
    pool->pthread_queue = NULL;
    pool->ptask_queue   = NULL;

    pool->add_task = add_task;
    pool->destory_pool = destory_pool;
    pool->_run_task = _run_task;

    return pool;
}

测试用的子任务

void* show() {
    int i;
    for (i = 0; i < 3; ++i) {
        printf("I am %lx ------ %d\n", pthread_self(), i);
        usleep(1);
    }
}

main函数调用

int main() {
    // 创建一个线程池
    cthread_pool_t* pool= create_pool();

    // 不断添加任务
    int i;
    for (i = 0; i < 10; i++) {
        pool->add_task(pool, show, NULL);
        usleep(1);
    }

    // 保证所有任务执行完毕
    sleep(3);

    // 销毁线程池
    pool->destory_pool(pool);

    return 0;
}

05 结果

New thread born.
New thread born.
I am 700000cfc000 ------ 0
I am 700000d7f000 ------ 0
New thread born.
I am 700000d7f000 ------ 1
I am 700000cfc000 ------ 1
I am 700000e02000 ------ 0
I am 700000d7f000 ------ 0
I am 700000e02000 ------ 1
I am 700000d7f000 ------ 1
I am 700000cfc000 ------ 0
I am 700000cfc000 ------ 1
I am 700000e02000 ------ 0
I am 700000d7f000 ------ 0
I am 700000cfc000 ------ 0
New thread born.
I am 700000cfc000 ------ 1
I am 700000d7f000 ------ 1
I am 700000e02000 ------ 1
I am 700000cfc000 ------ 0
I am 700000e85000 ------ 0
I am 700000cfc000 ------ 1
I am 700000e85000 ------ 1
One thread die.
One thread die.
One thread die.
One thread die.

Logo

更多推荐