C语言版简易线程池
背景
若同时有十万个客户端向服务器请求数据,那么我们所需要的资源:
posix标准的线程8M,则1G内存可以开128个线程,16G内存只能开16*128 = 2048个线程,则需要800G左右的内存,显然耗费硬件资源过大,所以需要一种解决方案。
线程池的优点
- 可以重用已存在的线程,避免线程太多,使得内存耗尽
- 避免创建和销毁线程的代价
- 任务与执行相分离
线程池实现的图示
线程池主要由三部分构成:任务队列、执行队列(线程队列)和管理组件
代码剖析
结构体定义
任务队列和线程队列都是采用了双向链表来实现:
//任务类型
struct nTask{
//任务实际工作函数
void (*task_func)(void *arg);
void *user_data; //task_func的参数
//任务队列用双向列表组织起来
struct nTask *prev;
struct nTask *next;
};
//线程(执行)类型
struct nWorker{
pthread_t threadid;
struct nManager *manager;
int terminate; //是否终止的标识.1为终止
struct nWorker *prev;
struct nWorker *next;
};
链表的插入删除操作分别用宏实现:
//支持层对链表的操作
#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)
**执行队列和任务队列之间的管理组件就是我们所谓的线程池,**结构体定义如下:
typedef struct nManager{
struct nTask *tasks; //任务队列
struct nWorker *workers;//执行队列
pthread_mutex_t mutex;
pthread_cond_t cond;
}ThreadPool;
线程池初始化
线程池初始化就是对 除任务队列tasks外(任务时外界添加进来的) 的其他成员进行初始化,在线程池中创建给定数量的线程:
//对线程池pool进行初始化,nWorker是线程池中线程数量(即聘请的柜员数量)
int nThreadPoolCreate(ThreadPool *pool, int numWorkers)
{
if(pool == NULL) return -1;
if(numWorkers < 1) numWorkers = 1;
//pool必须要初始化为空,如果是垃圾值下面memcpy会出现段错误
memset(pool, 0x00, sizeof(ThreadPool));
//相当于pthread_cond_init()进行初始化
pthread_cond_t blank_cond = PTHREAD_COND_INITIALIZER;
memcpy(&pool->cond, &blank_cond, sizeof(pthread_cond_t));
//pthread_mutex_init(&pool->mutex, NULL); // 初始化互斥锁,下方代码功能相同
pthread_mutex_t blank_mutex = PTHREAD_MUTEX_INITIALIZER;
memcpy(&pool->mutex, &blank_mutex, sizeof(pthread_mutex_t));
int i = 0;
//创建线程
for(i = 0;i < numWorkers; ++i)
{
struct nWorker *worker = (struct nWorker *)malloc(sizeof(struct nWorker));
if(worker == NULL)
{
perror("malloc");
return -2;
}
memset(worker, 0x00, sizeof(struct nWorker));
worker->manager = pool;//方便线程回调函数的使用
//创建线程,成功返回0
//线程回调函数为nThreadPoolCallback
//参数为:worker
int ret = pthread_create(&worker->threadid, NULL, nThreadPoolCallback, worker);
if(ret)
{
perror("pthread_create");
free(worker);
return -3;
}
//printf("LIST_INSERT\n");
//将线程插入线程链表(队列)头
LIST_INSERT(worker, pool->workers);
}
return 0;
}
线程回调函数实现
线程回调函数的主要功能就是循环等待任务的到来,有任务就执行,没有任务就循环等待:
static void *nThreadPoolCallback(void *arg)
{
//worker指向某一个线程
struct nWorker *worker = (struct nWorker *)arg;
//printf("nThreadPoolCallback\n");
//有任务执行任务,没任务循环等待
while(1)
{
//这里也能看出nWorker中添加了manager成员的用处,就不用再传nManager对象了
pthread_mutex_lock(&worker->manager->mutex);
//无任务时,阻塞等待
while(worker->manager->tasks == NULL)
{
//worker->terminate非0表明所有线程都要销毁了
if(worker->terminate) break;
//条件等待
//会先解除互斥锁让其他线程能够执行,然后阻塞,
//当被pthread_cond_signal唤醒时,则会解除阻塞并加互斥锁
pthread_cond_wait(&worker->manager->cond, &worker->manager->mutex);
}
if(worker->terminate)
{
//防止死锁
pthread_mutex_unlock(&worker->manager->mutex);
break;
}
//任务执行,将任务从任务队列中取出
struct nTask *task = worker->manager->tasks;
LIST_REMOVE(task, worker->manager->tasks);
pthread_mutex_unlock(&worker->manager->mutex);
//执行任务
task->task_func(task);
}
free(worker);
}
销毁所有线程
销毁所有线程就是让每一个线程的terminate属性为1,表明要退出,当对应线程回调函数执行到对于该属性判断的时候就会退出循环。
int nThreadPoolDestroy(ThreadPool *pool, int nWorker)
{
struct nWorker *worker = NULL;
for(worker = pool->workers;worker != NULL; worker = worker->next)
{
worker->terminate = 1; // 使得对应线程执行其回调函数就会退出
}
//保证任务执行的时候不会被直接打断(任务执行时也是用的这把互斥锁)
pthread_mutex_lock(&pool->mutex);
pthread_cond_broadcast(&pool->cond); // 广播唤醒所有等待任务的nWorker
pthread_mutex_unlock(&pool->mutex);
pool->workers = NULL;
pool->tasks = NULL;
return 0;
}
添加任务功能
添加任务就是将任务添加进任务链表,并且通过pthread_cond_signal
唤醒其中一个线程去执行这个任务:
int nThreadPoolPushTask(ThreadPool *pool, struct nTask *task)
{
//操作到任务队列和执行队列要加锁 (pool->tasks是共享资源)
pthread_mutex_lock(&pool->mutex);
//添加任务
LIST_INSERT(task, pool->tasks);
//通知回调函数有任务去执行任务
pthread_cond_signal(&pool->cond); //唤醒一个线程中的wait
pthread_mutex_unlock(&pool->mutex);
}
可能会出的错误
-
结构体变量没有进行清空,导致有值,解决方法:
ThreadPool pool = {0}
或者
ThreadPool pool; memset(&pool, 0x00, sizeof(ThreadPool));
-
主线程没有等待子线程执行任务结束就退出
解决办法:主线程出口位置添加 getchar()让用户输入一个字符再退出
思考题
了解CAS无锁算法,并写出自己心得体会
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.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)
//任务类型
struct nTask{
//任务实际工作函数
void (*task_func)(void *arg);
void *user_data; //task_func的参数
//任务队列用双向列表组织起来
struct nTask *prev;
struct nTask *next;
};
//线程(执行)类型
struct nWorker{
pthread_t threadid;
struct nManager *manager;
int terminate; //是否终止的标识.1为终止
struct nWorker *prev;
struct nWorker *next;
};
//执行队列和任务队列之间的管理组件
//这就是我们所谓的线程池,就是这个管理组件
typedef struct nManager{
struct nTask *tasks; //任务队列
struct nWorker *workers;//执行队列
pthread_mutex_t mutex;
pthread_cond_t cond;
}ThreadPool;
//线程回调函数,static表示本文件有效
//回调函数不等于任务!callback != task
//每个线程的任务就是循环等待任务到来并执行任务
static void *nThreadPoolCallback(void *arg)
{
//worker指向某一个线程
struct nWorker *worker = (struct nWorker *)arg;
printf("nThreadPoolCallback\n");
//有任务执行任务,没任务循环等待
while(1)
{
//这里也能看出nWorker中添加了manager成员的用处,就不用再传nManager对象了
pthread_mutex_lock(&worker->manager->mutex);
//无任务时,阻塞等待
while(worker->manager->tasks == NULL)
{
//worker->terminate表明所有线程都要销毁了
if(worker->terminate) break;
//条件等待
//会先解除互斥锁让其他线程能够执行,然后阻塞,
//当被pthread_cond_signal唤醒时,则会解除阻塞并加互斥锁
pthread_cond_wait(&worker->manager->cond, &worker->manager->mutex);
}
if(worker->terminate)
{
//防止死锁
pthread_mutex_unlock(&worker->manager->mutex);
break;
}
//任务执行,将任务从任务队列中取出
struct nTask *task = worker->manager->tasks;
LIST_REMOVE(task, worker->manager->tasks);
pthread_mutex_unlock(&worker->manager->mutex);
//执行任务
task->task_func(task);
}
free(worker);
}
//对线程池pool进行初始化,numWorkers是线程池中线程数量(即聘请的柜员数量)
int nThreadPoolCreate(ThreadPool *pool, int numWorkers)
{
if(pool == NULL) return -1;
if(numWorkers < 1) numWorkers = 1;
//pool必须要初始化为空,如果是垃圾值下面memcpy会出现段错误
memset(pool, 0x00, sizeof(ThreadPool));
//相当于pthread_cond_init()进行初始化
pthread_cond_t blank_cond = PTHREAD_COND_INITIALIZER;
memcpy(&pool->cond, &blank_cond, sizeof(pthread_cond_t));
//pthread_mutex_init(&pool->mutex, NULL); // 初始化互斥锁,下方代码功能相同
pthread_mutex_t blank_mutex = PTHREAD_MUTEX_INITIALIZER;
memcpy(&pool->mutex, &blank_mutex, sizeof(pthread_mutex_t));
int i = 0;
//创建线程
for(i = 0;i < numWorkers; ++i)
{
struct nWorker *worker = (struct nWorker *)malloc(sizeof(struct nWorker));
if(worker == NULL)
{
perror("malloc");
return -2;
}
memset(worker, 0x00, sizeof(struct nWorker));
worker->manager = pool;//方便操作
//创建线程,成功返回0
int ret = pthread_create(&worker->threadid, NULL, nThreadPoolCallback, worker);
if(ret)
{
perror("pthread_create");
free(worker);
return -3;
}
printf("LIST_INSERT\n");
//将线程插入线程链表(队列)头
LIST_INSERT(worker, pool->workers);
}
return 0;
}
int nThreadPoolDestroy(ThreadPool *pool, int nWorker)
{
struct nWorker *worker = NULL;
for(worker = pool->workers;worker != NULL; worker = worker->next)
{
worker->terminate = 1; // 使得对应线程执行其回调函数就会退出
}
//保证任务执行的时候不会被直接打断(任务执行时也是用的这把互斥锁)
pthread_mutex_lock(&pool->mutex);
pthread_cond_broadcast(&pool->cond); // 广播唤醒所有等待任务的nWorker
pthread_mutex_unlock(&pool->mutex);
pool->workers = NULL;
pool->tasks = NULL;
return 0;
}
//往任务队列添加任务
int nThreadPoolPushTask(ThreadPool *pool, struct nTask *task)
{
//操作到任务队列和执行队列要加锁 (pool->tasks是共享资源)
pthread_mutex_lock(&pool->mutex);
//添加任务
LIST_INSERT(task, pool->tasks);
//通知回调函数有任务去执行任务
pthread_cond_signal(&pool->cond); //唤醒一个线程中的wait
pthread_mutex_unlock(&pool->mutex);
}
#if 1
//线程池初始化线程数量
#define THREADPOOL_INIT_COUNT 20
#define TASK_INIT_SIZE 1000
void task_entry(void *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(void)
{
ThreadPool pool = {0};
//初始化线程池
nThreadPoolCreate(&pool, THREADPOOL_INIT_COUNT);
printf("nThreadPoolCreate -- finish\n");
int i = 0;
//添加任务
for(i = 0;i < TASK_INIT_SIZE;i++)
{
struct nTask *task = (struct nTask *)malloc(sizeof(struct nTask));
if(task == NULL)
{
perror("malloc");
exit(1);
}
memset(task, 0x00, sizeof(struct nTask));
task->task_func = task_entry;
task->user_data = malloc(sizeof(int));
//先转换为int *类型,再取得指向的值
*(int *)task->user_data = i;
nThreadPoolPushTask(&pool, task);
}
//为了防止主线程在子线程之前退出
//让用户输入一个字符
getchar();
}
#endif