目录
前言
这是我经过学习并理解线程池后,基于自己的理解完成的对于线程池的分析。因为是刚学和理解,相信对于初学者也有一定的帮助。
一、线程池是什么?
我们要学习线程池呢,首先要知道线程池是什么。再首次呢,要知道-->线程。线程我们知道,它是系统调度的最小单位,也就是可以独立执行的最小单位。那么线程池呢,顾名思义,它就是多个线程组成的集合,它的目的呢,就是为了并发的执行任务。
二、任务链表
1、任务链表由来
解释:相信很多同学和我一样,看到任务链表感觉很高级,但是它其实就是我们前面学习过的单链表,只不过呢,我们之前用单链表存的是整型,字符型等数据;而我们的任务链表呢,它存的是任务,那就有很多同学有疑问了,任务是啥?链表怎么存任务?那么现在呢,一个一个解答。
任务: 其实就是函数,因为我们很热爱封装函数来帮助我们实现各种功能,而线程就是并发执行函数。
链表怎么存任务(函数):我们知道,之前我们的链表存整型、字符型等数据类型,都是在数据域存储一种数据类型,这样使得数据域可以存储相同数据类型的任意数据。但是我们要利用链表来存储函数的话,是不是应该找一个通用的表示形式来表示所有函数呢,所以在这里,我们可以想到我们C语言的函数指针,但是呢,我们的函数有三个特征标:返回值,函数名,函数形参。所以函数用统一表现形式要克服三个特征标不一样的问题。但是是我们可以想到,我们之前学习了一个万能指针void*,那如果我们用这个来表示我们的函数返回值和函数形参,那我们是不是就可以表示所有函数呢?恭喜你,答对了。那么最后我们的函数统一表示形式就是: void* (*p)(void*).但是又有同学问了,为什么你的函数参数就一个一个指针呢,要是我的函数参数有多个怎么办呢,哈哈,同学们忘记了我们之前学过的结构体吗,我们把不同或者相同数据类型的函数参数封装成一个结构体,再进行传参,那我们就巧妙的解决了这个问题。
2、任务链表的实现
(1)任务链表结构体定义
//任务链表(单链表)
typedef struct tasklist
{
//数据域
void* (*taskp)(void*);//万能函数指针,存放相关任务
void* task_arg;//传给万能函数指针的参数
//指针域
struct tasklist* next;
}Task;
(2)任务链表初始化
//初始化任务链表头节点
Task* tasklist_init()
{
Task* head = malloc(sizeof(Task));
head->taskp = NULL;
head->task_arg = NULL;
head->next = NULL;
}
(3) 任务链表添加任务(尾插法)
//添加任务(尾插法)
int add_task(void* (*newtaskp)(void*), void *newtask_arg, Task* head, Threadpool* mypool)
{
Task* node = head;
Task* newnode = malloc(sizeof(Task));
newnode->next = NULL;
newnode->taskp = newtaskp;
newnode->task_arg = newtask_arg;
while(node->next != NULL)
{
node = node->next;
}
//上锁
pthread_mutex_lock(&(mypool->threadpool_mutex));
node->next = newnode;//尾插任务
mypool->tasknum++;//更新任务数量
//解锁
pthread_mutex_unlock(&(mypool->threadpool_mutex));
//唤醒某个线程,因为没有任务时线程都阻塞了
pthread_cond_signal(&(mypool->threadpool_cond));
return 0;
}
解释:这里有一个要强调的点就是,为什么要用互斥锁呢,因为我们知道我们的添加任务函数和待会儿我们要讲的线程池,有一个共享资源就是任务链表,为了协调它们对于任务链表的访问呢,我们就加入了互斥锁来确保任意时刻只有一个线程或者添加任务函数访问任务链表.
三、线程池
1.线程池的工作原理
解释: 前面我们讲到了,线程池是线程的集合,也就是说创建了多个线程,那么我们怎么来表示线程池的相关属性呢,答案肯定是我们的结构体。那应该包含什么呢? 我们知道,创建线程,代表线程的标志,就是它们的id号;其次呢,就是要协调每个线程对于共享资源的的访问,需要创建一把互斥锁;然后我们任务链表中没有任务了呢,线程需要阻塞,这时候就需要条件变量了;然后呢还需要统计任务链表个数来确定条件变量的判定条件;那是不是还有我们的最重要的"人物"-任务链表没有登场呢,对,所以需要保存任务链表的头节点;最后一个就是我们的标志位,来判断线程任务函数是否退出,这个呢这里说比较抽象,下面代码中有体现,相信你可以理解的。
2.线程池的实现
(1) 线程池结构体
//线程池结构体
typedef struct threadpool
{
pthread_t* id;//保存所有线程id号
pthread_mutex_t threadpool_mutex;//互斥锁协调线程池访问任务链表、协调线程访问任务链表和添加任务函数添加任务
pthread_cond_t threadpool_cond;//条件变量
int tasknum;//统计任务链表中的任务数量
Task* taskhead;//任务链表的头节点
bool shutdown;//添加标志量保证线程可以退出 true 线程池关闭 false 线程池打开
}Threadpo
(2)线程池初始化
hreadpool* pool_init(int num)
{
Threadpool* mypool = (Threadpool*)malloc(sizeof(Threadpool)*num);
mypool->id = malloc(sizeof(pthread_t)*num);//给存放线程id的指针分配空间
pthread_mutex_init(&(mypool->threadpool_mutex), NULL);//初始化互斥锁
pthread_cond_init(&(mypool->threadpool_cond), NULL);//初始化条件变量
mypool->tasknum = 0;//初始化任务数量
mypool->taskhead = NULL;//初始化指向任务链表结点的指针
mypool->shutdown = false;//线程开启
for(int i=0; i<num; i++)
{
/*
(1):每个线程在创建时会在系统中分配一个独立的执行上下文,包括栈、寄存器等。即使线程 ID 被覆盖,线程本身仍然在运行。
(2):线程的 ID 只是一个标识符,用于管理和控制线程。它并不影响线程的执行。
(3):线程创建后,除非是用相关函数或者主线程结束, 不然线程的执行不会结束
*/
pthread_create(&(mypool->id[i]), NULL, threadpool_operation, mypool);
}
return mypool;
}
(3)线程池的任务函数
//线程池取任务函数(从任务链表中取任务)
void* threadpool_operation(void* arg)
{
Threadpool* mypool = (Threadpool*)arg;
Task* node = NULL;
//负责从任务链表取任务执行(取了就删除)
while(1)
{
//上锁
pthread_mutex_lock(&(mypool->threadpool_mutex));
//刚开始时,没有任务就让条件变量阻塞线程
while(mypool->tasknum<=0 && mypool->shutdown==false)
{
pthread_cond_wait(&(mypool->threadpool_cond), &(mypool->threadpool_mutex));
}
//没有任务了,就退出线程取任务函数
if(mypool->tasknum<=0 && mypool->shutdown==true)
{
//解锁-->防止死锁
pthread_mutex_unlock(&(mypool->threadpool_mutex));
//线程退出
pthread_exit(0);
}
//取任务
node = mypool->taskhead->next;
//更新链表
mypool->taskhead->next = node->next;
//更新任务数量
mypool->tasknum--;
//解锁
pthread_mutex_unlock(&(mypool->threadpool_mutex));
//执行相关任务
(node->taskp)(node->task_arg);
//释放相关结点
free(node);
//初始化node指向
node = NULL;
}
//线程退出
pthread_exit(0);
return NULL;
}
解释:这部分代码呢,就是线程池的核心代码,为什么这么说呢?因为这里是线程池从任务链表中取任务执行,并且保证了每个线程没有任务就阻塞,任务链表中没有任务了就退出的代码。
(4)销毁线程池
//销毁线程池
void destroy_pool(Threadpool* mypool, int num)
{
//更新标志位让线程任务函数退出
mypool->shutdown = true;
//唤醒所有线程-》在此之前,任务执行完毕,全部线程被阻塞,因此要唤醒再判断循环条件
pthread_cond_broadcast(&mypool->threadpool_cond);
for(int i=0; i<num; i++)
{
errno = pthread_join(mypool->id[i], NULL);
if(errno == 0)
{
printf("id号为%ld的线程已经回收!\n",mypool->id[i]);
}
else
{
printf("id号为%ld的线程回收失败!\n",mypool->id[i]);
}
}
free(mypool->taskhead);//已经删除只剩下头节点了
free(mypool->id);
free(mypool);
return;
}