Linux中基于c/c++多线程编程学习杂记:c语言手写线程池

 

目录

 1. 线程池原理

2. 任务队列

3. 线程池定义

4. 头文件声明

5. 源文件定义

5.1实例化线程池创建函数

5.2 工作线程任务函数的实现

6. 测试代码


 1. 线程池原理

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢?

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

在各个编程语言的语种中都有线程池的概念,并且很多语言中直接提供了线程池,作为程序猿直接使用就可以了,下面给大家介绍一下线程池的实现原理:

  • 线程池的组成主要分为3个部分,这三部分配合工作就可以得到一个完整的线程池:

    1. 任务队列,存储需要处理的任务,由工作的线程来处理这些任务
      • 通过线程池提供的API函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
      • 已处理的任务会被从任务队列中删除
      • 线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程
    2. 工作的线程(任务队列任务的消费者) ,N个
      • 线程池中维护了一定数量的工作线程, 他们的作用是是不停的读任务队列, 从里边取出任务并处理
      • 工作的线程相当于是任务队列的消费者角色,
      • 如果任务队列为空, 工作的线程将会被阻塞 (使用条件变量/信号量阻塞)
      • 如果阻塞之后有了新的任务, 由生产者将阻塞解除, 工作线程开始工作
    3. 管理者线程(不处理任务队列中的任务),1个
      • 它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测
        • 当任务过多的时候, 可以适当的创建一些新的工作线程
        • 当任务过少的时候, 可以适当的销毁一些工作的线程

    Linux中基于c/c++多线程编程学习杂记:c语言手写线程池_第1张图片

2. 任务队列

任务队列是用来存储多个任务的,因此,最起码是一个数组或者一个链表;任务队列里面的任务是一个功能函数,功能函数在调用的时候是不是需要参数是不知道的。因此在设计的时候需要将参数设计出来;如果需要,使用这个参数。不需要就无视这个参数;所以任务队列里面的任务在存储的时候是一个复合结构;函数的地址是通过一个函数指针来存储的,因此第一个成员是函数指针,第二个参数是函数的实参。这两个组合在一起是一个复合类型; 因此,任务队列里面的任务是一个结构体;

// 任务结构体 
typedef struct Task
{
    //由于不知道参数是什么类型,故将其定义为一个泛型,即void *.以便让其使用更多的场景;
    //void *类型是一个泛型,可以兼容各种各样的数据类型。
    void (*function)(void* arg);
    //定义一个变量来保存函数中实参的地址
    void* arg;
}Task;

3. 线程池定义

#include "threadpool.h"
#include 
// 任务结构体 
typedef struct Task
{
    //由于不知道参数是什么类型,故将其定义为一个泛型,即void *.以便让其使用更多的场景;
    //void *类型是一个泛型,可以兼容各种各样的数据类型。
    void (*function)(void* arg);
    //定义一个变量来保存函数中实参的地址
    void* arg;
}Task;
//线程池的结构体
struct ThreadPool
{
    // 任务队列,其实就是多个任务的集合
    Task* taskQ;        //先定义一个指针,然后让这个指针指向结构体类型数组的首地址
    int queueCapacity;  // 任务队列的最大容量,即数组的最大值 
    int queueSize;      // 当前任务个数,即任务队列的大小 
    int queueFront;     // 队头 -> 取数据
    int queueRear;      // 队尾 -> 放数据

    //与线程池相关的
    //第一部分:两类线程,分别是工作线程和管理者线程;其中工作线程有N个,管理者线程只有一个;
    pthread_t managerID;    // 管理者线程ID
    pthread_t* threadIDs;   // 工作的线程ID,有n个,先定义一个指针,然后让这个指针指向数组的首地址
    //指定线程池中线程个数的范围
    int minNum;             // 最小线程数量
    int maxNum;             // 最大线程数量
    //记录当前有多少个线程工作
    int busyNum;            // 忙的线程的个数
    //定义当前总的存活的个数
    int liveNum;            // 存活的线程的个数
    //存活的多余工作的,剩下的要杀死
    int exitNum;            // 要销毁的线程个数

    //多个线程对线程池操作可能会出现数据混乱,故需要对整个任务队列用互斥锁保证线程同步
    pthread_mutex_t mutexPool;  // 锁整个的线程池
    //给操作频率较高的成员单独上锁
    pthread_mutex_t mutexBusy;  // 锁busyNum变量

    //若消费者将任务队列中的任务消耗完,消费者线程应该产生堵塞;此时就使用到了条件变量
    //添加条件变量
    pthread_cond_t notFull;     // 判定任务队列是不是满了
    pthread_cond_t notEmpty;    // 判定任务队列是不是空了

    //通过该成员判断当前线程池是否在工作
    int shutdown;           // 是不是要销毁线程池, 销毁为1, 不销毁为0
};   

4. 头文件声明

#ifndef _THREADPOOL_H
#define _THREADPOOL_H
const int NUMBER=2;

typedef struct ThreadPool ThreadPool;
// 创建线程池并初始化
ThreadPool *threadPoolCreate(int min, int max, int queueSize);

// 销毁线程池
int threadPoolDestroy(ThreadPool* pool);

// 给线程池添加任务
void threadPoolAdd(ThreadPool* pool, void(*func)(void*), void* arg);

// 获取线程池中工作的线程的个数
int threadPoolBusyNum(ThreadPool* pool);

// 获取线程池中活着的线程的个数
int threadPoolAliveNum(ThreadPool* pool);

//
// 工作的线程(消费者线程)任务函数
void* worker(void* arg);
// 管理者线程任务函数
void* manager(void* arg);
// 单个线程退出
void threadExit(ThreadPool* pool);
#endif  // _THREADPOOL_H

5. 源文件定义

5.1实例化线程池创建函数

//实例化线程池创建函数
ThreadPool* threadPoolCreate(int min, int max, int queueSize)
{
    //需要通过地址将线程池传递给其它任务函数,因此要保证地址不能够释放
    //故需要通过malloc方式来创建一块堆内存
    ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));//malloc函数的返回值是void*类型,故需要强制类型转换
    do
    {
        //判断pool指针指向的堆内存是否为空,若为空表示堆内存开辟失败
        if (pool == NULL)
        {
            printf("malloc threadpool failed...\n");
            //返回为空
            break;;
        }

        //初始化ThreadPool结构体中的成员
        // 
        //初始化工作者线程ID
        pool->threadIDs = (pthread_t*)malloc(sizeof(pthread_t) * max);
        if (pool->threadIDs == NULL)
        {
            printf("malloc threadIDs failed...\n");
            //返回为空
            break;
        }
        //若创建成功,对内存进行初始化
        memset(pool->threadIDs, 0, sizeof(pthread_t) * max); //初始化为0

        //初始化与线程个数相关的整型变量
        pool->minNum = min;
        pool->maxNum = max;
        pool->busyNum = 0;
        pool->liveNum = min; //初始化时,和最小个数相等
        pool->exitNum = 0;

        //不管是互斥锁还是条件变量,初始化时都需要调用对应的init方法
        if (pthread_mutex_init(&pool->mutexPool, NULL) != 0 ||
            pthread_mutex_init(&pool->mutexBusy, NULL) != 0 ||
            pthread_cond_innit(&pool->notEmpty, NULL) != 0 ||
            pthread_cond_innit(&pool->notFull, NULL) != 0)
        {
            printf("mutex or condition failed...\n");
           break;  //条件不满足将跳出do...while循环
        }

        //任务队列的初始化
        pool->taskQ = (Task *)malloc(sizeof(Task) * queueSize);
        pool->queueCapacity = queueSize;
        pool->queueSize = 0;
        //因为任务队列里面没有任务,故头和尾都指向同一个位置,都是0
        pool->queueFront = 0;
        pool->queueRear = 0;

        pool->shutdown = 0;

        //创建线程
        pthread_create(&pool->managerID, NULL, manager, pool);//管理者线程
        //创建工作线程,先按最小数来创建
        int i = 0;
        for (i = 0; i < min; ++i)
        {
            pthread_create(&pool->threadIDs[i], NULL, worker, pool);
        }
        //如果操作整体流程没有问题,返回线程池结构体的地址
        return pool; // 将pool返回给调用者即可
    } while (0);

    //进行资源的释放
    if (pool->threadIDs)
    {
        free(pool->threadIDs);//如果pool中threadIDs指向一块有效内存,将其释放掉
    }
    if (pool->taskQ)
    {
        free(pool->taskQ);
    }
    if (pool)
    {
        free(pool);
    }
    return NULL;
}

5.2 工作线程任务函数的实现

//工作线程任务函数的实现
void* worker(void* arg)
{
    //首先将传过来的线程池对象做一个类型转换。转换成threadPool类型
    ThreadPool* pool = (ThreadPool*)arg;
    //因为线程进入任务函数之后需要一直不停的去读任务队列,所以应该用一个循环实现
    while (1)
    {
        //每个线程都需要对线程池进行操作,因此线程池是一个共享资源,我们需要在访问线程池之前加锁
        //访问完之后解锁
        //添加加锁函数
        pthread_mutex_lock(&pool->mutexPool);
        //判断当前任务是否为空
        while (pool->queueSize ==0&&!pool->shutdown )//如果任务为0且线程池没有被关闭
        {
            //阻塞工作线程
            pthread_cond_wait(&pool->notEmpty, &pool->mutexPool);
            //判断是不是要销毁线程
            if (pool->exitNum > 0)
            {
                pool->exitNum--;
                pthread_mutex_unlock(&pool->mutexPool);
                //让线程自杀,即让线程退出
                pthread_exit(NULL);
            }
        }
        //判断当前线程池是否被关闭了
        if (pool->shutdown)
        {
            //避免死锁
            pthread_mutex_unlock(&pool->mutexPool);
            pthread_exit(NULL);

        }
        //从任务队列中取出一个任务
        Task task;
        task.function = pool->taskQ[pool->queueFront].function;
        task.arg = pool->taskQ[pool->queueFront].arg;
        //取出后,移动头结点。让queueFront向后移动,移动到最后位置后,跳到头部
        pool->queueFront = (pool->queueCapacity + 1) % pool->queueCapacity;
        //当前队列中,任务总个数减一
        pool->queueSize--;
        //解锁
        pthread_mutex_unlock(&pool->mutexPool);
        printf("thread %ld start working...\n");
        //当前线程工作。其他线程可能也在工作,因此,想给忙的线程变量加一,那么这个变量需要加锁
        pthread_mutex_lock(&pool->mutexBusy);
        //函数执行时,忙的线程加一
        pool->busyNum++;
        pthread_mutex_unlock(&pool->mutexBusy);
        //取出任务后,开始执行任务,调用任务函数
        task.function(task.arg);
        free(task.arg);
        task.arg = NULL;
        printf("thread %ld end working...\n");
        //任务执行完毕之后,需要将忙的线程值改回去
        pthread_mutex_lock(&pool->mutexBusy);
        pool->busyNum--;
        pthread_mutex_unlock(&pool->mutexBusy);
    }
    return NULL;
}

5.3 管理者线程任务函数

//管理者线程任务函数
void* manager(void* arg)
{
    ThreadPool* pool = (ThreadPool*)arg;
    //管理者需要不停的干活,所以需要将其写到一个循环中
    while (!pool->shutdown)//当线程池开启的时候干活
    {
        //每隔三秒检测一次
        sleep(3);
        //管理者线程主要用来创建和销毁线程的
        //取出线程池中任务数量和当前线程的数量
        //我们在读这些数据的时候,其他线程可能在写这些数据,因此需要加锁
        //该数据通过线程池的那把锁来锁定
        pthread_mutex_lock(&pool->mutexPool);
        //取出任务队列中任务个数
        int queueSize = pool->queueSize;
        //当前线程池存活的线程个数
        int liveNum = pool->exitNum;
        //取出工作的线程数量。也就是忙的线程数量
        int busyNum = pool->busyNum;
        pthread_mutex_unlock(&pool->mutexPool);

        //添加线程
        //当前任务个数大于存活线程个数时&&存活线程个数小于最大最大线程数的时候,添加线程
        //如果当前线程数已经是最大线程数的时候就不可以在继续添加了
        if (queueSize > liveNum && liveNum < pool->maxNum)  //判断什么时候需要添加线程
        {
            int counter = 0;
            //加锁,防止其它线程也执行pool->liveNum++操作
            pthread_mutex_lock(&pool->mutexPool);
            //一次最多创建两个线程, NUMBER为一次性添加的最大线程数量,此时为2
            for (int i = 0; i < pool->maxNum && counter < NUMBER && pool->liveNum < pool->maxNum;++i)
            {
                //从0到pool->maxNum中查找threadIDs数组中有哪一个元素可以用来存储线程ID
                if (pool->threadIDs[i] == 0)//该位置值为0,空闲状态,可以存储线程ID
                {
                    //条件满足,就创建线程ID
                    pthread_create(&pool->threadIDs[i], NULL, worker, pool);
                    counter++;
                    pool->liveNum++;
                }
            }
            //解锁
            pthread_mutex_unlock(&pool->mutexPool);
        }

       //销毁线程
       /*忙的线程*2<存活的线程数&&存活的线程数>最小线程数,即存活的线程多,
       干活的线程少,这样就可以销毁一部分线程*/
       //按照counter去销毁,一次销毁两个
        if (busyNum * 2 < liveNum && liveNum > pool->minNum)
        {
            //因为是线程池中的变量,因此,在操作他的时候需要加锁
            pthread_mutex_lock(&pool->mutexPool);
            pool->exitNum = NUMBER;//一次销毁两个线程
            pthread_mutex_unlock(&pool->mutexPool);
            //让工作的线程自杀
            for (int i = 0; i < NUMBER; ++i)
            {
                pthread_cond_signal(&pool->notEmpty);//唤醒工作的线程,工作的线程阻塞在notEmpty条件变量上的
                //signal是一次至少唤醒一个
            }
        }

    }
    return NULL;
}

5.4

5.5

6. 测试代码

void taskFunc(void* arg)
{
    int num = *(int*)arg;
    printf("thread %ld is working, number = %d\n",
        pthread_self(), num);
    sleep(1);
}

int main()
{
    // 创建线程池
    ThreadPool* pool = threadPoolCreate(3, 10, 100);
    for (int i = 0; i < 100; ++i)
    {
        int* num = (int*)malloc(sizeof(int));
        *num = i + 100;
        threadPoolAdd(pool, taskFunc, num);
    }

    sleep(30);

    threadPoolDestroy(pool);
    return 0;
}

你可能感兴趣的:(C++学习专栏,java,开发语言)