实现一个线程池确实主要围绕线程管理和任务管理两大核心模块来设计。
线程管理模块
线程管理模块主要负责线程的创建、销毁、以及线程的同步与通信。其核心职责包括:
- 线程创建:在线程池初始化时,根据配置参数预先创建一定数量的工作线程。
- 线程同步:确保线程安全,通常使用互斥锁、条件变量等机制来协调线程间的操作,如任务队列的访问。
- 线程空闲与等待:当任务队列为空时,工作线程应进入等待状态,避免不必要的资源消耗;有新任务到来时能够被唤醒并处理任务。
- 线程优雅退出:当线程池需要关闭时,能确保所有线程完成当前任务后安全退出。
任务管理模块
任务管理模块主要负责接收外部提交的任务,将其加入到任务队列中,并确保这些任务能被线程池中的线程正确执行。其核心职责包括:
- 任务队列:维护一个任务队列(如std::queue),用于存储待执行的任务。
- 任务添加:提供接口允许外部向线程池提交任务。
- 任务调度:当有新任务加入时,通知等待中的线程开始执行任务。
- 任务执行:确保任务能够从队列中取出并分配给线程执行。
很多例子使用std里面的vector和queue来管理线程和任务队列,虽然方便好用但是不是很能够深入,而且c++语言特性过多不利于理解线程池的核心思想,因此本文只使用C语言实现。
在写程序的时候应该先选择算法还是数据结构?个人认为是先选择数据结构,因为算法就是对数据进行操作,数据结构不一样,相同的功能写起来也不一样。比如链表的插入就和顺序表不一样。
初步确定的数据结构有三个:任务、任务队列、线程池。自旋锁用于避免线程在等待任务时陷入休眠状态从而减少上下文切换带来的开销,原子变量则是控制线程池的生命周期。
// 函数指针
typedef void (*handler_pt)(void *);
// 自旋锁
typedef struct spinlock spinlock_t;
// 任务
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;
};
创建任务队列:
需要注意的是初始化后队列尾指针也指向了头指针,队列也被设置为阻塞状态。
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 void __nonblock(task_queue_t *queue) {
pthread_mutex_lock(&queue->mutex);
queue->block = 0;
pthread_mutex_unlock(&queue->mutex);
pthread_cond_broadcast(&queue->cond);
}
任务队列添加任务:
有个地方比较难理解就是为什么*queue->tail等价于 queue->tail->next 。这个写法可以看下我以前写的文章c/c++指针解析。简单的来说就是二级指针进行解引用,可以控制从起始地址到起始地址+8个字节的空间,为什么时8个字节因为二级指针解引用后是一级指针,一级指针只占用8个字节的内存。这里task_s结构体的第一个成员变量是next,刚好是8个字节的指针。
static inline void __add_task(task_queue_t *queue, void *task) {
void **link = (void**)task;
*link = NULL;
spinlock_lock(&queue->lock);
/* *queue->tail等价于 queue->tail->next */
*queue->tail = link;
queue->tail = link;
spinlock_unlock(&queue->lock);
pthread_cond_signal(&queue->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;
void **link = (void**)task;
queue->head = *link;
if (queue->head == NULL) {
queue->tail = &queue->head;
}
spinlock_unlock(&queue->lock);
return task;
}
取出一个任务:
注意这个函数的目的是获取任务队列中的任务。与 __pop_task
不同的是,如果队列为空,它不会立即返回 NULL
,而是会进入等待状态。
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;
}
销毁任务队列:
销毁时释放所有资源
void thrdpool_waitdone(thrdpool_t *pool) {
int i;
for (i=0; i<pool->thrd_count; i++) {
pthread_join(pool->threads[i], NULL);
}
__taskqueue_destroy(pool->task_queue);
free(pool->threads);
free(pool);
}
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);
}
创建线程池:
static int __threads_create(thrdpool_t *pool, size_t thrd_count) {
pthread_attr_t attr;
int ret;
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;
}
终止线程池:
static void __threads_terminate(thrdpool_t * pool) {
atomic_store(&pool->quit, 1);
__nonblock(pool->task_queue);
int i;
for (i=0; i<pool->thrd_count; i++) {
pthread_join(pool->threads[i], NULL);
}
}
void thrdpool_terminate(thrdpool_t * pool) {
atomic_store(&pool->quit, 1);
__nonblock(pool->task_queue);
}
发布任务:
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 * __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;
}
这是一条吃饭推文,由挨踢零声赞助。源代码来自挨踢零声