LinuxC多线程简单实现线程池
1. 线程池的简单介绍
1. 什么是线程池
大家都知道水池是来装水的,那么很明显,线程池就是用来装线程的。以便整体对线程进行创建和回收。
2. 线程池的优势
- 避免创建线程过多,使得系统资源耗尽
- 减小系统不断创建和销毁线程的代价
- 使得任务与执行分离
2. 线程池实现
废话不多说,直接上代码:
#include <stdio.h>
#include <pthread.h>
#include <malloc.h>
#include <string.h>
#include <stdlib.h>
#define LIST_INSERT(item, list) do \
{ \
item->prev = NULL; \
item->next = list; \
if ((list) != NULL) (list)->prev = item; \
list = item; \
} while (0)
#define LIST_REMOVE(item, list) do \
{ \
if (item->prev != NULL) item->prev->next = item->next; \
if (item->next != NULL) item->next->prev = item->prev; \
if (list == item) list = item->next; \
item->prev = item->next = NULL; \
} while (0)
/*
1. 任务队列
2. 执行队列
3. 管理队列 --> 锁
*/
/*
一个组件:需要先定任务
*/
struct nTask
{
void (*task_func)(struct nTask *arg);
void *user_data;
struct nTask *prev;
struct nTask *next;
};
struct nWorker
{
pthread_t threadid;
int terminate;
struct nManager *manager;
struct nWorker *prev;
struct nWorker *next;
};
typedef struct nManager
{
struct nTask *tasks;
struct nWorker *workers;
pthread_mutex_t mutex;
pthread_cond_t cond; //条件变量
} ThreadPool; // 定义线程池
void *nThreadPoolCallback(struct nWorker *worker)
{
struct nWorker *curWorker = worker;
while (1)
{
pthread_mutex_lock(&curWorker->manager->mutex);
while (curWorker->manager->tasks == NULL)
{
if (curWorker->terminate)
{
break;
}
pthread_cond_wait(&curWorker->manager->cond, &curWorker->manager->mutex);
}
if (curWorker->terminate)
{
pthread_mutex_unlock(&curWorker->manager->mutex);
break;
}
struct nTask *task = curWorker->manager->tasks;
LIST_REMOVE(task, curWorker->manager->tasks);
pthread_mutex_unlock(&curWorker->manager->mutex);
task->task_func(task);
}
free(curWorker);
}
int nThreadPoolCreate(ThreadPool *pool, int numWorkers)
{
if (pool == NULL) return -1;
if (numWorkers < 1) numWorkers = 1;
memset(pool, 0, sizeof(ThreadPool));
pthread_cond_t blank_cont = PTHREAD_COND_INITIALIZER;
memcpy(&pool->cond, &blank_cont, sizeof(pthread_cond_t));
pthread_mutex_init(&pool->mutex, NULL);
for (int ii = 0; ii < numWorkers; ++ii)
{
struct nWorker * worker = (struct nWorker *)malloc(sizeof(struct nWorker));
if (worker == NULL)
{
perror("malloc");
return -2;
}
memset(worker, 0, sizeof(struct nWorker));
worker->manager = pool;
int result = pthread_create(&worker->threadid, NULL, nThreadPoolCallback, worker);
if (result)
{
perror("pthread_create");
free(worker);
return -3;
}
printf("Create thread %d\n", ii);
LIST_INSERT(worker, pool->workers);
}
return 0;
}
int nThreadPoolDestory(ThreadPool *pool, int numWorkers)
{
for (struct nWorker *worker = pool->workers; worker != NULL; worker = worker->next)
{
worker->terminate = 1;
}
pthread_mutex_lock(&pool->mutex);
pthread_cond_broadcast(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
pool->workers = NULL;
pool->tasks = NULL;
return 0;
}
int nThreadPoolPushTask(ThreadPool *pool, struct nTask *task)
{
pthread_mutex_lock(&pool->mutex);
// printf("insert task: %d\n", *(int *)task->user_data);
LIST_INSERT(task, pool->tasks);
pthread_cond_signal(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
}
#if 1
#define THREADPOOL_INIT_COUNT 200
#define TASK_INIT_SIZE 1000000
void task_entry(struct nTask *arg)
{
struct nTask *task = (struct nTask *)arg;
int idx = *(int *)task->user_data;
printf("idx: %d\n", idx);
free(task->user_data);
free(task);
}
int main()
{
ThreadPool pool;
nThreadPoolCreate(&pool, THREADPOOL_INIT_COUNT);
printf("Test thread pool is start\n");
for (int ii = 0; ii < TASK_INIT_SIZE; ++ii)
{
struct nTask *task = (struct nTask *)malloc(sizeof(struct nTask));
if (task == NULL)
{
perror("malloc");
exit(1);
}
memset(task, 0, sizeof(struct nTask));
task->task_func = task_entry;
task->user_data = malloc(sizeof(int));
*(int *)task->user_data = ii;
nThreadPoolPushTask(&pool, task);
}
getchar();
}
#endif
3. 代码详解
1. 定义线程池结构
struct nTask
{
void (*task_func)(struct nTask *arg);
void *user_data;
struct nTask *prev;
struct nTask *next;
};
struct nWorker
{
pthread_t threadid;
int terminate;
struct nManager *manager;
struct nWorker *prev;
struct nWorker *next;
};
typedef struct nManager
{
struct nTask *tasks;
struct nWorker *workers;
pthread_mutex_t mutex;
pthread_cond_t cond; //条件变量
} ThreadPool; // 定义线程池
- nManager 为线程池的管理组件
该组件时线程池的核心,包含任务队列 tasks、执行队列 workers、互斥锁以及与互斥锁配合使用的条件变量。
- nTask 为任务结构体
包含一个函数指针,为需要执行的任务;用户数据,为执行任务需要用到的数据,和链表的前后节点。
- nWorker 为执行结构体
包含一个线程 ID ,一个终止标志 terminate,以及管理组件(因为执行需要对管理组件进行操作),和链表的前后节点。
2. 线程池创建
int nThreadPoolCreate(ThreadPool *pool, int numWorkers)
{
if (pool == NULL) return -1;
if (numWorkers < 1) numWorkers = 1;
memset(pool, 0, sizeof(ThreadPool));
pthread_cond_t blank_cont = PTHREAD_COND_INITIALIZER;
memcpy(&pool->cond, &blank_cont, sizeof(pthread_cond_t));
pthread_mutex_init(&pool->mutex, NULL);
for (int ii = 0; ii < numWorkers; ++ii)
{
struct nWorker * worker = (struct nWorker *)malloc(sizeof(struct nWorker));
if (worker == NULL)
{
perror("malloc");
return -2;
}
memset(worker, 0, sizeof(struct nWorker));
worker->manager = pool;
int result = pthread_create(&worker->threadid, NULL, nThreadPoolCallback, worker);
if (result)
{
perror("pthread_create");
free(worker);
return -3;
}
printf("Create thread %d\n", ii);
LIST_INSERT(worker, pool->workers);
}
return 0;
}
上述程序为线程池创建的函数,输出参数为线程池管理组件,以及需要创建的线程数量。在上述程序中:
- 7-10 行初始化互斥锁和条件变量
- 23 行创建线程,其中 nThreadPoolCallback 为回调函数,参数为 worker。
- 31 将执行结构体插入任务队列
- 14-31完成一个线程的创建
3. 线程池回调函数
void *nThreadPoolCallback(struct nWorker *worker)
{
struct nWorker *curWorker = worker;
while (1)
{
pthread_mutex_lock(&curWorker->manager->mutex);
while (curWorker->manager->tasks == NULL)
{
if (curWorker->terminate)
{
break;
}
pthread_cond_wait(&curWorker->manager->cond, &curWorker->manager->mutex);
}
if (curWorker->terminate)
{
pthread_mutex_unlock(&curWorker->manager->mutex);
break;
}
struct nTask *task = curWorker->manager->tasks;
LIST_REMOVE(task, curWorker->manager->tasks);
pthread_mutex_unlock(&curWorker->manager->mutex);
task->task_func(task);
}
free(curWorker);
}
上述代码为线程的回调函数,在系统没有告知线程结束前,回调函数会一直循环调用,代码具体结构为:
- 第9-11以及15-19行为判断线程是否结束的判断语句,当主线程告知线程需要结束,被销毁时,terminate 置为1,回调函数退出,不再执行,线程也会随之终止。
- 第21行为获取一个任务,任务获取之后将会被当前线程执行。在获取任务执行,该条任务也从任务队列中擦除
- 第6和第23行为对线程上锁,因为6-23行之间是给从线程池的任务队列中给当前执行者分配任务的,如果不加锁,其他的线程可能会导致将同一个任务分配给不同的执行者(也就是不同的线程),导致出错。因此此处需要给队列加锁。
- 第13行为等待条件变量变为真,当调降变量为假时,线程会在此处等待,然后释放互斥锁,其他线程同样运行到此处等待,知道外界发送信号条件变量为真,某一个线程获得互斥锁,继续执行,其他线程继续在此处等待。
- 第24行为执行业务的函数调用,该调用不用加锁,因为每个任务并不共享数据,每个任务线程可以单独进行,彼此独立,不受线程之间的影响。
- 关于线程结束退出:当外界将种植条件置为1时,线程退出,如果线程处于等待条件变量的状态,还需要外加发送信号将条件变量置为真。
4. 任务队列
int nThreadPoolPushTask(ThreadPool *pool, struct nTask *task)
{
pthread_mutex_lock(&pool->mutex);
// printf("insert task: %d\n", *(int *)task->user_data);
LIST_INSERT(task, pool->tasks);
pthread_cond_signal(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
}
上述代码为将任务插入管理组件的任务队列中,每插入一个,触发条件变量,某个线程获得互斥锁,继续执行程序,也就是会执行相应的任务。
5. 线程资源回收
int nThreadPoolDestory(ThreadPool *pool, int numWorkers)
{
for (struct nWorker *worker = pool->workers; worker != NULL; worker = worker->next)
{
worker->terminate = 1;
}
pthread_mutex_lock(&pool->mutex);
pthread_cond_broadcast(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
pool->workers = NULL;
pool->tasks = NULL;
return 0;
}
上述代码为线程资源回收的程序,当调用该函数后,首先会执行队列中的所有执行者的终止标志全部置为1,然后向所有的线程发送信号,条件变量全部变为真,所有线程一次获得互斥锁,回收资源。
6. 测试主函数
#if 1
#define THREADPOOL_INIT_COUNT 200
#define TASK_INIT_SIZE 1000000
void task_entry(struct nTask *arg)
{
struct nTask *task = (struct nTask *)arg;
int idx = *(int *)task->user_data;
printf("idx: %d\n", idx);
free(task->user_data);
free(task);
}
int main()
{
ThreadPool pool;
printf("Test thread pool is start\n");
nThreadPoolCreate(&pool, THREADPOOL_INIT_COUNT);
for (int ii = 0; ii < TASK_INIT_SIZE; ++ii)
{
struct nTask *task = (struct nTask *)malloc(sizeof(struct nTask));
if (task == NULL)
{
perror("malloc");
exit(1);
}
memset(task, 0, sizeof(struct nTask));
task->task_func = task_entry;
task->user_data = malloc(sizeof(int));
*(int *)task->user_data = ii;
nThreadPoolPushTask(&pool, task);
}
getchar();
}
#endif
该代码为测试程序
- 第3、4行为定义线程数和任务数量
- 第6-13为任务函数,为具体需要执行的操作
- 第15-36行为主函数,其中首先创建线程池。然后依次创建任务,将任务插入任务队列
- 第37行为等待键盘输入一个字符结束程序,不然的话程序结束过早会产生错误。
4. 回顾
让我们来回顾一下程序实现了什么。
首先我们假设一个场景,在银行中,有三个柜员可以处理各种任务,银行开门之后,来了大量的顾客,顾客依次到预约系统上排队拿号,等待办理业务,当喇叭喊到客户的名字时,即可在相应的柜台办理业务。
在上述假设中,我们银行柜员是就是执行者,对应 nWorker 结构体,客户的业务即为执行者需要执行的任务即为 nTask 结构体,而银行的系统为管理组件 nManager 结构体。因此上述程序可以形象的表示为:
- 银行上班了,柜员A、B、C坐到工位上,等待来办理业务的人(也就是73行 pthread_cond_wait(&curWorker->manager->cond, &curWorker->manager->mutex);
- 来了一个客户,需要办理业务,银行系统将客户分配到一个柜员A为其办理业务(第84行 task->task_func(task);),其他柜员继续等待。
- 高峰期到了,来了10个客户,银行系统一次将10个客户加入了任务队列,并分配了两个客户给空余柜员B、C。
- 此时,第一个客户业务办理完成,柜员A伸了伸拦腰,心想可以休息一会儿了(第67行 while (curWorker->manager->tasks == NULL)),但是一看,哎,怎么还有人,于是又只能继续为等待办理业务的人办理。
- 终于所有的客户都办理完成,柜员A、B、C终于可以休息一会了,于是又在等待。
- 非常幸运,没有客户再到来,银行也到了下班时间,主管通知柜员A、B、C 可以下班了(第133行 pthread_cond_broadcast(&pool->cond);)。
- 柜员A、B、C 收拾好工位,关闭柜台窗口,离开工位回家了。
上述程序中,实现了一个简单的线程池,首先是批量的申请创建了一些线程,然后用这些线程对任务进行处理,再向线程池中加入任务需要访问共享资源之外,各个任务的执行可以独立进行,实现并发。再现实中,基本不可能所有的线程都同时运行,就想世界上又很多的电话,但是所有电话同时在通话的可能性为0。因此,我们的应用中,完全不需要有多少个任务就开多少个线程,只需要又适当的线程可供使用即可。
5. —.-
上述文字纯属个人理解,错误的地方希望各位大佬指正。