C语言写一个线程池
这个是我很久之前写的了,今天发出来做一下纪念,顺便回顾一下线程池的知识。
线程池的原理
想象地面上有一群鸽子(类比于进程),我们手里拿着面包屑(类比于任务)喂他们,当我们扔出一个面包屑的时候(类比于出现了一个任务),鸽子们蜂拥而上,抢夺那个面包屑,最终只有一个鸽子能吃到面包屑,而其他的鸽子则继续等待新的面包屑的到来。
上面的这个过程就是线程池的原理。
我们创建多个线程,让他们进入等待状态,然后再创建一个任务队列,当任务队列中存在任务时,线程们会去取得任务的执行权限(大部分情况下是去抢夺一块内存空间),之后通过互斥锁告诉其他线程该任务已经被抢到了,其他线程则继续抢夺队列中的其他任务,直到为空为止。
线程池示例
接下来,我们以输出一个文件中每一行的内容作为任务为例子,写一个线程池的代码。
首先,明确我们的任务:
获取文件中第一行未获取的字符串,并打印出来。
那该如何设计呢?首先是需要获取任务,我们可以让一个进程去获取文件中每一行的字符的空间地址,作为一个任务放进队列中,之后让线程们去输出。这是我们的基本思路。
任务队列的设计
接下来我们就需要设计任务队列了,根据我们的需求,任务队列可以这样设计:
struct task_queue{
int size; // 队列大小
int total; // 队列当前任务个数
int head; // 头指针
int tail; // 尾指针
char **data; // 数据地址
pthread_mutex_t mutex; // 锁
pthread_cond_t cond; // 信号量
};
相关函数设计
我们对于相关函数的设计都是基于任务队列实现的,具体如下:
- 任务队列的初始化
- 在任务队列中添加任务
- 弹出任务
- 运行任务
因此有下面四个函数:
void task_queue_init(struct task_queue *taskQueue, int size);
void task_queue_push(struct task_queue *taksQueue, char *str);
char *task_queue_pop(struct task_queue *taskQueue);
void *thread_run(void *arg);
以上就是头文件中的内容:
#ifndef _THREAD_POOL_H
#define _THREAD_POOL_H
struct task_queue{
int size;
int total;
int head;
int tail;
char **data;
pthread_mutex_t mutex;
pthread_cond_t cond;
};
void task_queue_init(struct task_queue *taskQueue, int size);
void task_queue_push(struct task_queue *taksQueue, char *str);
char *task_queue_pop(struct task_queue *taskQueue);
void *thread_run(void *arg);
#endif
函数实现
任务队列初始化
这个函数的功能主要是为了确定队列的大小以及头尾指针的位置,确保后面的其他相关函数能在队列上正确运行。
void task_queue_init(struct task_queue *taskQueue, int size) {
taskQueue->size = size;
taskQueue->total = taskQueue->head = taskQueue->tail = 0;
pthread_mutex_init(&taskQueue->mutex, NULL);
pthread_cond_init(&taskQueue->cond, NULL);
taskQueue->data = calloc(size, sizeof(void *));
return ;
}
添加任务
回想一下刚才设计的任务内容,每个线程是用于输出一串字符串的,字符串从文件中获取。因此我们在任务队列中需要传入每个字符串的地址。
需要注意的是,在添加任务时,也需要注意空间访问的竟态性,也就是说我们需要加上锁。
同时,仅仅是添加任务不行,还需要让线程们知道队列中有任务。因此需要使用条件变量告诉线程们这里有任务。
使用轮询替代条件变量可以吗?不可以,因为线程池存在的意义是用来提高程序效率的,如果使用轮询,那么会出现很多线程在CPU上“空转”的情况,这样会大大降低效率,吃力不讨好。
void task_queue_push(struct task_queue *taskQueue, char *str) {
// 加锁
pthread_mutex_lock(&taskQueue->mutex);
// 任务队列满
if (taskQueue->total == taskQueue->size) {
// 解锁并且返回
printf(RED"taskQueue is full!\n"NONE);
pthread_mutex_unlock(&taskQueue->mutex);
return ;
}
// 在队列尾部添加字符串地址
taskQueue->data[taskQueue->tail] = str;
taskQueue->total++; // 队列任务数量加一
// 注意需要维护循环队列的性质
if (++taskQueue->tail == taskQueue->size) {
taskQueue->tail = 0;
}
printf(GREEN"<D> data is pushed to taskQueue!\n"NONE);
pthread_cond_signal(&taskQueue->cond); // 告诉线程们存在任务
pthread_mutex_unlock(&taskQueue->mutex); // 释放空间
}
弹出任务
这个函数用于从任务队列的头部弹出任务,也就是起到分发任务的作用。
同样要注意,任务的获取也是多线程中对任务空间的竟态访问,因此需要加锁
还有一点,这里与鸽子吃面包不同的是,线程们并不是去争抢一个任务,而是排队进行,具体原因是体现在pthread_cond_wait
这个函数上。
简单来说,在上一个函数的代码中,倒数第二行代码发送了一个信号:
pthread_cond_signal(&taskQueue->cond); // 告诉线程们存在任务
但实际上,这个信号只会告诉一个线程让其摆脱阻塞状态,也就是说,只有一个线程会通过pthread_cond_wait
函数收到信号。然后去执行这个任务。
char *task_queue_pop(struct task_queue *taskQueue) {
pthread_mutex_lock(&taskQueue->mutex);
// 核心语句需要好好理解
while (taskQueue->total == 0) {
// 释放锁并等待信号
pthread_cond_wait(&taskQueue->cond, &taskQueue->mutex);
}
// 维护队列的性质
// 获取字符串
char *str = taskQueue->data[taskQueue->head];
taskQueue->total--;
printf("<D> taskQueue pop: %s\n", str);
if (++taskQueue->head == taskQueue->size) {
printf(RED"<D> taskQueue touched tail\n"NONE);
taskQueue->head = 0;
}
pthread_mutex_unlock(&taskQueue->mutex);
return str;
}
任务的运行
这个函数规定了每个线程需要做的任务,简单来说就是从任务队列中取出任务并且输出。
在这里需要注意的是,在线程池中,线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放(自己清理掉PCB的残留资源),因此我们需要执行线程分离,也就是pthread_detach()
函数。
void *thread_run(void *arg) {
// 首先执行线程分离
pthread_detach(pthread_self());
// 得到任务队列的地址
struct task_queue *taskQueue = (struct task_queue *)arg;
while (1) {
// 从任务队列中取出任务
char *str = task_queue_pop(taskQueue);
printf("%s\n", str);
}
}
总结一下代码:
thread_pool.h
:
#ifndef _THREAD_POOL_H
#define _THREAD_POOL_H
struct task_queue{
int size;
int total;
int head;
int tail;
char **data;
pthread_mutex_t mutex;
pthread_cond_t cond;
};
void task_queue_init(struct task_queue *taskQueue, int size);
void task_queue_push(struct task_queue *taksQueue, char *str);
char *task_queue_pop(struct task_queue *taskQueue);
void *thread_run(void *arg);
#endif
thread.pool.c
:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include "./thread_pool.h"
void task_queue_init(struct task_queue *taskQueue, int size) {
taskQueue->size = size;
taskQueue->total = taskQueue->head = taskQueue->tail = 0;
pthread_mutex_init(&taskQueue->mutex, NULL);
pthread_cond_init(&taskQueue->cond, NULL);
taskQueue->data = calloc(size, sizeof(void *));
return ;
}
void task_queue_push(struct task_queue *taskQueue, char *str) {
pthread_mutex_lock(&taskQueue->mutex);
if (taskQueue->total == taskQueue->size) {
DBG(RED"<D> taskQueue is full!\n"NONE);
pthread_mutex_unlock(&taskQueue->mutex);
return ;
}
taskQueue->data[taskQueue->tail] = str;
taskQueue->total++;
if (++taskQueue->tail == taskQueue->size) {
taskQueue->tail = 0;
}
DBG(GREEN"<D> data is pushed to taskQueue!\n"NONE);
pthread_cond_signal(&taskQueue->cond);
pthread_mutex_unlock(&taskQueue->mutex);
}
char *task_queue_pop(struct task_queue *taskQueue) {
pthread_mutex_lock(&taskQueue->mutex);
while (taskQueue->total == 0) {
pthread_cond_wait(&taskQueue->cond, &taskQueue->mutex);
}
char *str = taskQueue->data[taskQueue->head];
taskQueue->total--;
DBG(GREEN"<D> taskQueue pop: %s\n"NONE, str);
if (++taskQueue->head == taskQueue->size) {
DBG(RED"<D> taskQueue touched tail\n"NONE);
taskQueue->head = 0;
}
pthread_mutex_unlock(&taskQueue->mutex);
return str;
}
void *thread_run(void *arg) {
pthread_detach(pthread_self());
struct task_queue *taskQueue = (struct task_queue *)arg;
while (1) {
char *str = task_queue_pop(taskQueue);
printf("%s\n", str);
}
}
main.c
:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include "./thread_pool.h"
#define THREAD_NUM 10i // 线程数量
#define QUEUE_SIZE 100 // 队列长度
#define MAX_BUFF_LENGTH 1024 // 缓冲区长度
int main(){
// 线程id
pthread_t tid[THREAD_NUM];
struct task_queue taskQueue;
// buff用于记录文件中每一行的内容
char buff[QUEUE_SIZE + 2][MAX_BUFF_LENGTH] = {0};
task_queue_init(&taskQueue, QUEUE_SIZE);
// 创建线程并且让他们跑起来
for (int i = 0; i < THREAD_NUM; i++) {
pthread_create(&tid[i], NULL, thread_run, (void *)&taskQueue);
}
// 分发任务
int sub = 0; // sub用于记录buff的下标
while(1) {
// 这里以读取线程池的文件为例子
FILE *fp = fopen("./thread_pool.c", "r");
if (fp == NULL) {
perror("fopen");
exit(1);
}
// 每次取得一行
while(fgets(buff[sub], MAX_BUFF_LENGTH, fp) != NULL) {
// 放入任务队列
task_queue_push(&taskQueue, buff[sub]);
if (++sub >= QUEUE_SIZE) sub = 0;
}
// 任务队列满了,需要过一段时间再放入,这个时候让主线程睡一会
if (taskQueue.total == taskQueue.size) {
while (1) {
usleep(1000000);
if (taskQueue.total < taskQueue.size) break;
}
sleep(1);
}
fclose(fp);
}
return 0;
}