一、池式组件
池式组件是一种常用的软件设计模式,旨在提高资源利用率和系统性能。通过维护一个资源对象的集合(即“池”),并根据需要向请求者分配和回收这些资源,从而避免了频繁创建和销毁资源所带来的开销。这些资源对象可以是线程、内存、数据库连接等。因此分为线程池、内存池、数据库连接池。
二、线程池
线程池是一种多线程处理形式,用于有效利用服务器上的线程资源。它允许线程多次复用,且每次复用的线程可以处理不同的任务,从而节省创建与销毁线程的开销。
线程池的作用
- 降低资源消耗:线程创建和销毁是一个耗时的过程,通过线程池可以避免频繁的线程创建和销毁,从而降低资源消耗。
- 提高相应速度:当有新任务到达时,如果线程池中有空闲的线程,任务可以立即被执行。
- 提高线程的客观理性:通过线程池可以方便的控制同时运行的线程数量。当线程过多时会导致资源竞争和性能下降。
- 异步执行耗时任务:多个耗时任务可以由多个消费者线程分别执行操作。
线程数量选择
耗时任务可以分为两大类:IO密集型和CPU密集型。
IO密集型:该类任务主要受到输入/输出操作的限制。CPU在这些任务中大部分时间都在等待IO操作的完成。如网络数据传输、文件读写操作、数据库查询。
CPU密集型:该类任务主要受到CPU计算能力的限制。需要大量的CPU时间来执行复杂的计算或算法。如复杂的数学计算,图像和视频处理,大规模数据排序和搜索。
(io等待时间+cpu运算时间)*核心数/cpu运算时间。根据公式,一般CPU密集型的线程数量为CPU内核数;IO密集型的线程池线程数量等于2倍核心数+2;
构成
生产者线程:发布任务,将任务添加到任务队列里。
任务队列:用于存放任务,任务先进先出。任务的内容包括任务的上下文以及任务的执行函数。
消费者线程(线程池):是固定数量线程的集合,负责从任务队列中取出任务,并执行任务。
三、代码实现
结构体
//任务
typedef struct task_s {
void *next;
handler_pt func;
void *arg;
} task_t;
//任务队列
typedef struct task_queue_s {
void *head;
void **tail;
int block;
spinlock_t lock;
pthread_mutex_t mutex;
pthread_cond_t cond;
} task_queue_t;
//线程池
struct thrdpool_s {
task_queue_t *task_queue;
atomic_int quit;
int thrd_count;
pthread_t *threads;
};
结构体任务有三个参数:next指针指向下一个任务,func是任务的执行函数,以及arg任务的上下文执行函数的参数。
任务队列有六个参数:head指向头节点任务,二级指针tail指向末尾任务,block是队列的阻塞状态,lock是自旋锁,mutex是互斥锁,cond是条件变量。tail用二级指针为了添加任务时更好操作,后文会详细介绍。
条件变量用来进行线程的调度:线程有两种状态即从没有任务到有任务,从有任务到没有任务。利用条件变量,当队列有任务时唤醒线程;当没有任务时将线程休眠。
线程池有四个参数:任务队列,线程池状态(运行或终止),线程数量,线程;
接口设计
创建线程池
创建任务队列
static task_queue_t *
__taskqueue_create() {
int ret;
//为任务队列分配内存
task_queue_t *queue = (task_queue_t *)malloc(sizeof(task_queue_t));
//如果分配成功
if (queue) {
//初始化互斥锁
ret = pthread_mutex_init(&queue->mutex, NULL);
//初始化成功
if (ret == 0) {
//初始化条件变量
ret = pthread_cond_init(&queue->cond, NULL);
//如果初始化成功
if (ret == 0) {
//初始化自旋锁
spinlock_init(&queue->lock);
queue->head = NULL;
queue->tail = &queue->head;
queue->block = 1;//队列为空设置为阻塞
return queue;
}
//如果创建失败销毁
pthread_mutex_destroy(&queue->mutex);
}
free(queue);
}
return NULL;
}
添加任务
static inline void
__add_task(task_queue_t *queue, void *task) {
// 将task转换为二级指针,task的起始内存位置为指向下一个链表节点的指针
void **link = (void**)task;
//初始化下一个任务为空
*link = NULL;
//加锁以保护队列的并发
spinlock_lock(&queue->lock);
//tail为二级指针,因此*queue->tail 就是queue->tail->next,将原来最后一个任务的next指针指向新任务
*queue->tail /* 等价于 queue->tail->next */ = link;
//队列末尾指针指向新任务
queue->tail = link;
//解锁
spinlock_unlock(&queue->lock);
//发送条件变量信号,通知可能在等待新任务到来的线程
pthread_cond_signal(&queue->cond);//唤醒在cond上休眠的线程
}
在这段代码中,体现了为什么tail要用二级指针。
利用条件变量,当有任务时,唤醒在cond上休眠的线程。
弹出头部任务
static inline void *
__pop_task(task_queue_t *queue) {
//加锁
spinlock_lock(&queue->lock);
//如果队列为空解锁并返回错误值
if (queue->head == NULL) {
spinlock_unlock(&queue->lock);
return NULL;
}
//有任务时,获取头部任务的地址
task_t *task;
task = queue->head;
//link指向第二个任务。*link为第二个任务的地址
void **link = (void**)task;
queue->head = *link;
//如果第二个任务为空,设置尾指针指向空
if (queue->head == NULL) {
queue->tail = &queue->head;
}
//解锁
spinlock_unlock(&queue->lock);
//返回头部任务
return task;
}
获取头部任务(给消费者线程使用)
static inline void *
__get_task(task_queue_t *queue) {
task_t *task;
// 虚假唤醒,检查是否有事件。可能是系统内的信号唤醒的线程
//如果弹出任务
while ((task = __pop_task(queue)) == NULL) {
//加锁
pthread_mutex_lock(&queue->mutex);
if (queue->block == 0) {//判断是不是阻塞的,如果是非阻塞的返回出去
pthread_mutex_unlock(&queue->mutex);
return NULL;
}//关闭线程池的时候会把队列设置为非阻塞的
//如果是阻塞的,消费者线程等待取任务
// 1. 先 unlock(&mtx)
// 2. 在 cond 休眠
// --- __add_task 时唤醒
// 3. 在 cond 唤醒
// 4. 加上 lock(&mtx);
pthread_cond_wait(&queue->cond, &queue->mutex);
//解锁
pthread_mutex_unlock(&queue->mutex);
}
return task;
}
线程函数
static void *
__thrdpool_worker(void *arg) {
thrdpool_t *pool = (thrdpool_t*) arg;
task_t *task;
void *ctx;
//当线程池运行时
while (atomic_load(&pool->quit) == 0) {
//获取队首的任务
task = (task_t*)__get_task(pool->task_queue);
if (!task) break;
//获取任务函数
handler_pt func = task->func;
//获取参数
ctx = task->arg;
//销毁任务
free(task);
//执行任务
func(ctx);
}
return NULL;
}
创建线程
static int
__threads_create(thrdpool_t *pool, size_t thrd_count) {
pthread_attr_t attr;//用于描述线程的属性
int ret;
//初始化attr,将attr设置为默认的线程属性
ret = pthread_attr_init(&attr);
//如果初始化成功
if (ret == 0) {
//为所有线程创建内存
pool->threads = (pthread_t *)malloc(sizeof(pthread_t) * thrd_count);
//如果创建成功
if (pool->threads) {
int i = 0;
for (; i < thrd_count; i++) {
//创建每个线程
if (pthread_create(&pool->threads[i], &attr, __thrdpool_worker, pool) != 0) {
break;
}
}
pool->thrd_count = i;
pthread_attr_destroy(&attr);
//如果创建成功
if (i == thrd_count)
return 0;
//创建失败
__threads_terminate(pool);
free(pool->threads);
}
ret = -1;
}
return ret;
}
创建并初始化线程池(接口)
thrdpool_t *
thrdpool_create(int thrd_count) {
thrdpool_t *pool;
pool = (thrdpool_t*)malloc(sizeof(*pool));
if (pool) {
//创建任务队列
task_queue_t *queue = __taskqueue_create();
if (queue) {
pool->task_queue = queue;
//设置线程池运行
atomic_init(&pool->quit, 0);
//创建线程
if (__threads_create(pool, thrd_count) == 0)
return pool;
//如果创建失败则销毁
__taskqueue_destroy(queue);
}
free(pool);
}
return NULL;
}
提交新任务(接口)
int
thrdpool_post(thrdpool_t *pool, handler_pt func, void *arg) {
//如果线程池不在运行,返回错误值
if (atomic_load(&pool->quit) == 1)
return -1;
//为新任务分配内存
task_t *task = (task_t*) malloc(sizeof(task_t));
if (!task) return -1;
task->func = func;
task->arg = arg;
//将新任务添加到任务队列
__add_task(pool->task_queue, task);
return 0;
}
终止线程池
将任务队列设置为非阻塞,并唤醒所有线程
static void
__nonblock(task_queue_t *queue) {
pthread_mutex_lock(&queue->mutex);
queue->block = 0;//将任务队列设置为非阻塞
pthread_mutex_unlock(&queue->mutex);
pthread_cond_broadcast(&queue->cond);//广播条件变量来唤醒所有等待的线程
}
终止(接口)
void
thrdpool_terminate(thrdpool_t * pool) {
atomic_store(&pool->quit, 1);
__nonblock(pool->task_queue);
}
销毁线程池
销毁
static void
__taskqueue_destroy(task_queue_t *queue) {
task_t *task;
//弹出任务队列所有任务
while ((task = __pop_task(queue))) {
free(task);
}
//销毁自旋锁,互斥锁,条件变量,任务队列
spinlock_destroy(&queue->lock);
pthread_cond_destroy(&queue->cond);
pthread_mutex_destroy(&queue->mutex);
free(queue);
}
等待线程完成销毁线程池(接口)
void
thrdpool_waitdone(thrdpool_t *pool) {
int i;
for (i=0; i<pool->thrd_count; i++) {
//阻塞调用join的线程,直到线程终止。是同步机制,确保线程间的正确执行顺序
pthread_join(pool->threads[i], NULL);
}
//销毁任务队列
__taskqueue_destroy(pool->task_queue);
//销毁线程,线程池
free(pool->threads);
free(pool);
}