Threadpool
- 前言
由于最近的项目需求,服务器端用MQTT 下发的命令,并不能马上执行完成,需要等待完成。每次执行命令都需要单独创建任务,执行完成在释放任务,效率太慢了。所以准备手写一份线程池处理。网络收集了一些C语言版本的线程池资料,基本上都是基于 linux 或者windos 自带<pthreadpool.h>线程操作函数所编写的。最后是参考视频资料,以任务方式重写一遍基于 RTOS 系统的线程池库函数。参考资料:https://www.bilibili.com/video/BV1jV411J795?p=6&spm_id_from=333.999.header_right.history_list.click&vd_source=3c0bb11f7267a381f3e9e4934427f5f3
视频中以数组方式实现,缺点是有固定大小,需要维护数组的头尾指针位置。所以改用链表的方式实现,参考资料:https://www.cnblogs.com/skywang12345/p/3562146.html
其核心思想分为三大块:
- 主线程,也就是线程池的管理者线程,专门用来创建和回收子线程
- 子线程,也称为消费者线程,专门用来处理用户下发的任务函数
- 任务列表,维护用户添加的任务和参数
- 数据定义
线程池的结构体定义,此定义维护了线程池正常运行的关键参数
// 线程池结构体
typedef struct strThreadPoolMain
{
list_head listSon; // 子线程链表头
list_head listTask; // 任务链表头
ST_Thread *handle; // 主线程句柄
int minNum; // 最小线程数
int maxNum; // 最大线程数
int busyNum; // 忙碌线程数
int liveNum; // 存活线程数
int exitNum; // 销毁线程数
int shutdown; // 是否需要销毁线程池, 销毁为1,不销毁为0
ST_Mutex *mutexPool; // 锁整个线程池
ST_Mutex *mutexBusy; // 锁 busyNum 变量
ST_Semaphore *notEmpty; // 任务队列是否空了
list_head list; // 链表
} strThreadPoolMain;
子线程结构体,此结构体维护正常消费者线程的运行参数
// 子线程结构体
typedef struct strThreadPoolSon
{
int id; // 子线程ID
ST_Thread *handle; // 子线程句柄
strThreadPoolMain *pool; // 线程池结构体指针
list_head list; // 链表
} strThreadPoolSon;
任务结构体,主要是保存需要用线程执行的任务以及参数
// 任务结构体
typedef struct strThreadPoolTask
{
void (*func)(void *arg); // 任务地址
void *arg; // 任务参数
list_head list; // 链表
} strThreadPoolTask;
其链表维护的关系如图所示
- 函数实现
- 创建线程池 ThreadPool_create 函数
主要是申请函数线程池结构体空间以及初始化,另外就是主线程和子线程的创建。
/*******************************************************************************
* @name ThreadPool_create
* @brief Creating a thread pool
* @param[in] min minimum number of threads in the thread pool
* @param[in] max maximum number of threads in the thread pool
* @retval threadpool_t * a pointer to a thread pool
******************************************************************************/
strThreadPoolMain* ThreadPool_create(int min, int max)
{
strThreadPoolMain* pool = (strThreadPoolMain*)MG_MEM_Malloc(sizeof(strThreadPoolMain));
do
{
if (pool == NULL)
{
APP_Printf("malloc strThreadPoolMain fail ...\n");
return NULL;
}
MG_UTILS_Memset(pool, 0, sizeof(strThreadPoolMain));
INIT_LIST_HEAD(&pool->listTask); //任务链表头
INIT_LIST_HEAD(&pool->listSon); //线程链表头
pool->minNum = min;
pool->maxNum = max;
pool->mutexPool = MG_OS_MutexCreate();
pool->mutexBusy = MG_OS_MutexCreate();
pool->notEmpty = MG_OS_SemaphoreCreate(max, 0);
if(pool->mutexPool==NULL || pool->mutexBusy==NULL || pool->notEmpty==NULL)
{
APP_Printf("create mutex or semaphore fail ...\n");
break;
}
MG_OS_MutexLock(pool->mutexPool, OS_DELAY_MAX);
// 主线程创建
pool->handle = MG_OS_ThreadCreate("admin thread", (FU_osThreadEntry)ThreadPool_main, pool, THREAD_PRIO, THREAD_STK, THREAD_EVENT_CNT);
if (pool->handle == NULL)
{
APP_Printf("MG_OS_ThreadCreate(admin thread) fail ...\n");
break;
}
// 子线程创建
int i=0;
for (i=0; i<min; i++)
{
strThreadPoolSon *listSon = (strThreadPoolSon*)MG_MEM_Malloc(sizeof(strThreadPoolSon));
if(listSon == NULL)
{
APP_Printf("malloc listSon fail ...\n");
break;
}
char buff[64];
MG_UTILS_Snprintf((u8*)buff, sizeof(buff), "work[%d] pthread", id);
listSon->handle = MG_OS_ThreadCreate(buff, (FU_osThreadEntry)ThreadPool_son, listSon, THREAD_PRIO, THREAD_STK, THREAD_EVENT_CNT);
if (listSon->handle == NULL)
{
APP_Printf("MG_OS_ThreadCreate(%s) fail ...\n", buff);
break;
}
listSon->id = id++;
listSon->pool = pool;
list_add_tail(&listSon->list, &(pool->listSon));
pool->liveNum++;
}
if (i < min) break;
MG_OS_MutexUnlock(pool->mutexPool);
return pool;
} while (0);
MG_OS_MutexUnlock(pool->mutexPool);
// 释放资源
ThreadPool_free(pool);
return NULL;
}
1、首先是申请线程池结构体(strThreadPoolMain)空间,并清零。
2、任务链表和线程链表,先要初始化指向自己本身。
3、创建两把互斥锁, mutexPool 是整个线程池的互斥锁,让线程池变成顺序执行。mutexBusy是变量 busyNum 的互斥锁,因为这个变量是高频操作的,所以单独加锁提高执行效率。
4、创建信号量 notEmpty ,主要是当没有任务的时候,用来阻塞子线程。当需要销毁线程池的时候,每次释放一个信号量,缓存一个子线程自毁,所以信号量最大值取线程池最大值。
5、MG_OS_MutexLock(pool->mutexPool, OS_DELAY_MAX);
这里给线程池加锁,主要是防止在创建子线程的过程中,主线程或者子线程抢占了CPU运行,导致出差。当然这种概率发生的情况比较小。
6、当每个子线程创建完成后,调用list_add_tail(&listSon->list, &(pool->listSon)); 将新创建的子线程结构体嵌入到链表末尾。
7、创建成功后,返回线程池的指针。
8、若创建失败,调用ThreadPool_free(pool)销毁申请的资源,并返回NULL。
- 销毁资源 ThreadPool_free 函数
主要是释放创建线程池的时候,所申请的资源
/*******************************************************************************
* @name ThreadPool_free
* @brief Release route pool resources
* @param[in] pool the thread pool you want to destroy
* @retval void
******************************************************************************/
static void ThreadPool_free(strThreadPoolMain *pool)
{
pool->shutdown = 1;
if (pool->handle)
{
// 阻塞回收管理者线程
while (pool->shutdown != 2)
{// 系统调度20ms一次
MG_OS_ThreadSleep(40);
}
}
if (pool->liveNum)
{
// 唤醒阻塞的消费者线程
for (int i=0; i<pool->liveNum; i++)
{
MG_OS_SemaphoreRelease(pool->notEmpty);
}
// 阻塞回收消费者线程
while (1)
{
if(pool->liveNum == 0) break;
MG_OS_SemaphoreRelease(pool->notEmpty);
MG_OS_ThreadSleep(40);
}
}
if (pool->mutexPool) MG_OS_MutexDelete(pool->mutexPool);
if (pool->mutexBusy) MG_OS_MutexDelete(pool->mutexBusy);
if (pool->notEmpty) MG_OS_SemaphoreDelete(pool->notEmpty);
if (pool) MG_MEM_Free(pool);
pool = NULL;
}
这里 while (pool->shutdown != 2) 之后,需要休眠40ms
因为调用者的线程与创建的线程有可能是同级别的优先级,会导致一致抢占CPU,管理者线程得不到运行无法退出线程。另外线程调度是20ms一次,就能保证至少有一次给管理者线程执行的机会。
子线程每次唤醒都会自己退出,每次退出 liveNum 变量都会减一,当为0的时候表示所有的子线程退出完毕,if(pool->liveNum == 0) break;
剩下的就是释放互斥锁和信号量的资源。
- 销毁资源 ThreadPool_destroy 函数
其实就是调用 ThreadPool_free 函数销毁线程池资源
/*******************************************************************************
* @name ThreadPool_destroy
* @brief Destroy thread pool
* @param[in] pool the thread pool you want to destroy
* @retval int error code
******************************************************************************/
int ThreadPool_destroy(strThreadPoolMain *pool)
{
if (pool == NULL)
{
return -1;
}
// 释放资源
ThreadPool_free(pool);
return 0;
}
- 子线程 ThreadPool_son 函数
其实就是消费者线程,不停的从任务队列取出任务并执行。
/*******************************************************************************
* @name ThreadPool_son
* @brief Execute task function
* @param[in] arg pointer to child thread
* @retval void * NULL
******************************************************************************/
static void *ThreadPool_son(void *arg)
{
strThreadPoolSon *listSon = (strThreadPoolSon*)arg;
strThreadPoolMain *pool = listSon->pool;
while (1)
{
MG_OS_MutexLock(pool->mutexPool, OS_DELAY_MAX);
// 当前任务队列是否为空
while (list_empty(&pool->listTask) && !pool->shutdown)
{
// 阻塞工作线程
MG_OS_MutexUnlock(pool->mutexPool);
MG_OS_SemaphoreAcquire(pool->notEmpty, OS_DELAY_MAX);
MG_OS_MutexLock(pool->mutexPool, OS_DELAY_MAX);
// 判断是否需要销毁线程
if (pool->exitNum > 0)
{
pool->exitNum--;
if(pool->liveNum > pool->minNum)
{
goto _exit;
}
}
}
// 判断线程池是否被关闭了
if (pool->shutdown)
{
_exit:
pool->liveNum--;
APP_Printf("The ID of thread %lu is %d, exit\n", listSon->handle, listSon->id);
list_del(&listSon->list);
MG_MEM_Free(listSon);
MG_OS_MutexUnlock(pool->mutexPool);
MG_OS_ThreadExit();
}
// 从任务队列中取出一个任务
list_head *none = pool->listTask.next;
if (none == NULL)
{
MG_OS_MutexUnlock(pool->mutexPool);
}
else
{
strThreadPoolTask task;
MG_UTILS_Memcpy(&task, list_entry(none, strThreadPoolTask, list), sizeof(strThreadPoolTask));
list_del(none);
MG_MEM_Free(list_entry(none, strThreadPoolTask, list));
MG_OS_MutexUnlock(pool->mutexPool);
// 开始工作
APP_Printf("The ID of thread %lu is %d, start working..\n", listSon->handle, listSon->id);
MG_OS_MutexLock(pool->mutexBusy, OS_DELAY_MAX);
pool->busyNum++;
MG_OS_MutexUnlock(pool->mutexBusy);
task.func(task.arg);
// 结束工作
APP_Printf("The ID of thread %lu is %d, end working..\n", listSon->handle, listSon->id);
MG_OS_MutexLock(pool->mutexBusy, OS_DELAY_MAX);
pool->busyNum--;
MG_OS_MutexUnlock(pool->mutexBusy);
}
}
return NULL;
}
1、这里用while 不用 if ,主要是保证每次唤醒子线程,一定是有任务的情况下往下执行,否则有可能同时唤醒两个子线程,只有一个任务,则会出错。
while (list_empty(&pool->listTask) && !pool->shutdown)
- 如果有线程库文件的话,可以使用条件变量,pthread_cond_wait(&pool->notEmpty, &pool->mutexPool); 一句话就能执行阻塞动作。现在只能是用三句话,使用信号量来完成阻塞子线程,
MG_OS_MutexUnlock(pool->mutexPool);
MG_OS_SemaphoreAcquire(pool->notEmpty, OS_DELAY_MAX);
MG_OS_MutexLock(pool->mutexPool, OS_DELAY_MAX);
3、如果有需要销毁子线程操作,或者线程池被销毁了,会自动调整到 _exit 处,释放资源,退出任务。
4、取任务执行的时候,这里使用的是链表的经典宏定义 list_entry
list_entry(none, strThreadPoolTask, list) ,计算结构体 strThreadPoolTask 中,元素list的偏移量,然后用 none 的地址减去偏移,既得到该结构体 strThreadPoolTask 变量的真实地址。拷贝出来后,再删除该none节点,释放该变量资源。
5、剩下的就是直接调用函数指针,正真执行用户传进来的任务。busyNum 这个变量,在执行加减操作的时候,有个专门的互斥锁(mutexBusy),进行保护。
- 主线程 ThreadPool_main 函数
线程管理者函数,主要功能是管理线程的创建和销毁动作。
/*******************************************************************************
* @name ThreadPool_main
* @brief Create and destroy threads
* @param[in] arg a pointer to a thread pool
* @retval void * NULL
******************************************************************************/
static void *ThreadPool_main(void *arg)
{
strThreadPoolMain *pool = (strThreadPoolMain*)arg;
while (!pool->shutdown)
{
// 每隔1秒间隔工作一次
MG_OS_ThreadSleep(1000);
// 取出线程池中任务的数量和当前线程的数量
MG_OS_MutexLock(pool->mutexPool, OS_DELAY_MAX);
list_head *node, *temp;
int taskSize = 0;
int liveNum = 0;
int busyNum = 0;
list_for_each_safe (node, temp, &(pool->listTask))
{
taskSize++;
}
// list_for_each_safe (node, temp, &(pool->listSon))
// {
// liveNum++;
// }
liveNum = pool->liveNum; // <- 取出活的线程的数量
busyNum = pool->busyNum; // <- 取出忙的线程的数量
MG_OS_MutexUnlock(pool->mutexPool);
// 添加线程 (任务的个数 > 存活的线程个数 && 存活的线程 < 最大线程数)
if (taskSize>liveNum && liveNum<pool->maxNum)
{
MG_OS_MutexLock(pool->mutexPool, OS_DELAY_MAX);
for (int i=0; i<NUMBER && pool->liveNum<pool->maxNum; i++)
{
strThreadPoolSon *listSon = (strThreadPoolSon*)MG_MEM_Malloc(sizeof(strThreadPoolSon));
if(listSon == NULL)
{
APP_Printf("malloc listSon fail ...\n");
break;
}
char buff[64];
MG_UTILS_Snprintf((u8*)buff, sizeof(buff), "work[%d] pthread", id);
listSon->handle = MG_OS_ThreadCreate(buff, (FU_osThreadEntry)ThreadPool_son, listSon, THREAD_PRIO, THREAD_STK, THREAD_EVENT_CNT);
if(listSon->handle == NULL)
{
APP_Printf("MG_OS_ThreadCreate(%s) fail ...\n", buff);
break;
}
listSon->id = id++;
listSon->pool = pool;
list_add_tail(&listSon->list, &(pool->listSon));
pool->liveNum++;
}
MG_OS_MutexUnlock(pool->mutexPool);
}
// 销毁线程 (忙的线程*2 < 存活的线程 && 存活的线程 > 最小线程数)
if (busyNum * 2 < liveNum && liveNum > pool->minNum)
{
MG_OS_MutexLock(pool->mutexPool, OS_DELAY_MAX);
pool->exitNum = NUMBER;
MG_OS_MutexUnlock(pool->mutexPool);
// 让工作的线程自杀
for (int i=0; i<NUMBER; i++)
{
MG_OS_SemaphoreRelease(pool->notEmpty);
}
}
}
pool->shutdown = 2;
APP_Printf("pool->shutdown = %d\n", pool->shutdown);
APP_Printf("MG_OS_ThreadExit(%#X);\n", pool->handle);
MG_OS_ThreadExit();
return NULL;
}
管理者线程不需要频繁执行,每间隔几秒后,动态的调整当前子线程的数量即可。
- 这里调用的是链表宏定义list_for_each_safe (node, temp, &(pool->listTask)),遍历整个链表,计算任务数量。活着的子线程也可以这样操作,但是直接获取 liveNum 执行效率更快。
- 创建子线程的条件,可以按自己需求制定策略。这里以任务数量大于存活子线程的时候,每次创建两个子线程。
3、具体的创建子线程过程与初始化的时候一样,当每个子线程创建完成后,调用list_add_tail(&listSon->list, &(pool->listSon)); 将新创建的子线程结构体嵌入到链表末尾。
4、销毁线程的条件,可以按自己需求制定策略。这里以忙碌的线程两倍数量,还是小于存活的子线程数量,开始销毁子线程,每次销毁两个子线程。销毁的时候,将exitNum变量赋值为2 ,然后通过信号量唤醒子线程,由子线程自己释放结构体资源,退出线程。
5、管理者线程在正常工作的时候不会退出,只有当需要销毁线程池的时候,pool->shutdown 有效,先赋值为2通知其调用着,本主线程已经退出。
- 添加任务ThreadPool_add_task 函数
/*******************************************************************************
* @name ThreadPool_add_task
* @brief Add task function
* @param[in] pool a pointer to a thread pool
* @param[in] func address of task function
* @param[in] arg parameters of task function
* @retval int error code
******************************************************************************/
int ThreadPool_add_task(strThreadPoolMain *pool, void(*func)(void*), void *arg)
{
strThreadPoolTask *listTask = (strThreadPoolTask*)MG_MEM_Malloc(sizeof(strThreadPoolTask));
if (listTask == NULL)
{
APP_Printf("malloc listTask fail ...\n");
return -1;
}
listTask->func = func;
listTask->arg = arg;
MG_OS_MutexLock(pool->mutexPool, OS_DELAY_MAX);
list_add_tail(&listTask->list, &(pool->listTask));
MG_OS_MutexUnlock(pool->mutexPool);
MG_OS_SemaphoreRelease(pool->notEmpty);
return 0;
}
- 首先是申请内存空间,将用户传递进来的函数和参数都保存起来
- 在加锁 mutexPool 锁定整个线程池,将新增加的任务插入到任务链表的末尾后解锁
3、最后释放信号量,唤醒消费者线程执行任务。
- 获取忙碌线程个数ThreadPool_busy_num 函数
加锁后获取 busyNum 变量再解锁
/*******************************************************************************
* @name ThreadPool_busy_num
* @brief Get the number of busy threads in the thread pool
* @param[in] pool a pointer to a thread pool
* @retval int number of busy threads
******************************************************************************/
int ThreadPool_busy_num(strThreadPoolMain *pool)
{
MG_OS_MutexLock(pool->mutexBusy, OS_DELAY_MAX);
int busyNum = pool->busyNum;
MG_OS_MutexUnlock(pool->mutexBusy);
return busyNum;
}
- 获取存活线程个数ThreadPool_alive_num 函数
加锁后获取 liveNum变量再解锁
/*******************************************************************************
* @name ThreadPool_alive_num
* @brief Get the number of threads surviving in the thread pool
* @param[in] pool a pointer to a thread pool
* @retval int number of alive threads
******************************************************************************/
int ThreadPool_alive_num(strThreadPoolMain *pool)
{
MG_OS_MutexLock(pool->mutexPool, OS_DELAY_MAX);
int aliveNum = pool->liveNum;
MG_OS_MutexUnlock(pool->mutexPool);
return aliveNum;
}
最后上测试代码,创建线程池,最小3个线程,最大10个线程,创建100个任务。
struct taskData
{
int id;
int num;
};
void taskFunc(void *arg)
{
struct taskData *data = (struct taskData*)arg;
// APP_Printf("thread %ld is working, number = %d\n", pthread_self(), num);
APP_Printf("thread data->id is %ld working, number = %d\n", data->id, data->num);
MG_OS_ThreadSleep(1000);
}
int testThreadPool(void)
{
// 创建线程池
APP_Printf("start test thread pool\n");
strThreadPoolMain *pool = ThreadPool_create(3, 10);
for (int i=0; i<100; i++)
{
struct taskData *data = (struct taskData*)MG_MEM_Malloc(sizeof(struct taskData));
data->id = i;
data->num = i+100;
ThreadPool_add_task(pool, taskFunc, data);
}
MG_OS_ThreadSleep(25*1000);
for (int i=200; i<210; i++)
{
struct taskData *data = (struct taskData*)MG_MEM_Malloc(sizeof(struct taskData));
data->id = i;
data->num = i+200;
ThreadPool_add_task(pool, taskFunc, data);
}
MG_OS_ThreadSleep(5*1000);
ThreadPool_destroy(pool);
APP_Printf("end test thread pool\n");
return 0;
}
最后实测效果如下:
刚开始以最小任务运行,每次执行3个任务,当1秒钟后主线程唤醒,创建了2个新的线程,变成5个线程同时执行任务。直到循环创建满线程10个一起工作。
最后当所有任务执行完毕后,关闭了7个子线程,保留最小3个子线程阻塞等待任务。当执行销毁线程池动作后,会先关闭主线程,然后关闭所有子线程,最后释放互斥锁和信号量,完整退出销毁线程池动作。
初步看,该线程池能工作正常,能满足需求了。
资源下载地址:线程池C语言RTOS版本-嵌入式文档类资源-优快云下载
Hankin
2022.07.26