1.为啥要用线程池
避免线程频繁的创建销毁导致资源开销较大,可以提前创建一定数量的线程以供需要的时候随去随用。
2.线程池可以用在哪些地方
现代服务器都使用epoll、select这样的I/O多路复用技术——它们让CPU在等待网络数据时不会闲着,可以服务其他客户端,从而大幅提升并发性能
存在并发任务处理且需要控制线程资源的场景:
-
网络服务器(如 Web 服务器、Socket 服务器):需要处理大量客户端的并发请求(如 HTTP 请求、TCP 连接),线程池可快速分配线程处理请求,避免为每个请求单独创建线程导致的资源耗尽。
-
数据库服务器:处理多用户的数据库查询、事务操作等并发任务,通过线程池控制数据库连接和任务处理的并发度。
3.如何构建一个线程池
1.首先需要构建三个结构体(线程池关键的三个部分)
任务结构体:一个先进先出的队列
struct nTask {
void (*task_func)(struct nTask *task); // 存储“要执行的函数”和“函数的参数”
void *user_data; // 用户数据
struct nTask *prev; // 前驱指针,用于链表操作
struct nTask *next; // 后继指针,用于链表操作
};
工作者线程结构体:用于从任务队列取任务并执行
struct nWorker {
pthread_t threadid; // 线程ID
int terminate; // 终止标志
struct nManager *manager; // 指向线程池管理器
struct nWorker *prev; // 前驱指针,用于链表操作
struct nWorker *next; // 后继指针,用于链表操作
};
线程池管理器结构体:线程池的管理者,它用于给任务队列添加任务交给工作者线程执行
互斥锁:切换线程,锁的内容较多
自旋锁:等待时间小于切换的时间,锁的内容较少
原子操作:把三条指令变为一条指令,单条CPU指令xaddl
CAS :compare and swap
typedef struct nManager {
struct nTask *tasks; // 任务链表头指针
struct nWorker *workers; // 工作者线程链表头指针
pthread_mutex_t mutex; // 互斥锁,保护共享数据
pthread_cond_t cond; // 条件变量,用于线程同步
} ThreadPool;
2.宏定义链表的插入和删除
#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)
3.线程池的创建初始化
pthread_create 的第四个参数类型是 void*(无类型指针)—— 这是 C 语言的 “通用参数” 设计:不管你要传什么类型的数据(int、结构体、指针等),都能转换成 void* 传进去,让 pthread_create 能适配各种场景。
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_cond = PTHREAD_COND_INITIALIZER;
memcpy(&pool->cond, &blank_cond, sizeof(pthread_cond_t));
// 初始化互斥锁(使用静态初始化方式)
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, 0, sizeof(struct nWorker));
worker->manager = pool; // 设置线程池管理器指针
// 创建工作者线程
int ret = pthread_create(&worker->threadid, NULL, nThreadPoolCallback, worker);
if (ret) {
perror("pthread_create");
free(worker);
return -3; // 线程创建失败
}
// 将工作者线程插入链表
LIST_INSERT(worker, pool->workers);
}
return 0; // 创建成功
}
4.回调函数
异步执行,不影响主程序的操作
线程创建好不一定有任务,所以线程需要提前判断是否有任务存在,如果没有任务,那么继续阻塞状态继续等待,while循环暂停。若有任务则先会被唤醒,while循环继续,然后接受到任务去执行它。直到接受到终止信号退出,释放内存。
struct nWorker *worker = (struct nWorker*)arg;
struct nWorker* 类型的指针(指向你用 malloc 申请的 “工作者线程内存”,里面存着线程 ID、所属线程池、运行状态等信息)。
传递时的逻辑:struct nWorker* worker → 转换成 void* 类型 → 作为 arg 传给 nThreadPoolCallback 函数。
static void *nThreadPoolCallback(void *arg) {
struct nWorker *worker = (struct nWorker*)arg; // 获取工作者线程信息
while (1) {
// 加锁访问共享数据
pthread_mutex_lock(&worker->manager->mutex);
// 等待条件:有任务可执行或收到终止信号
while (worker->manager->tasks == NULL) {
if (worker->terminate) break; // 如果收到终止信号,退出等待
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);
return NULL;
}
5.向线程池添加任务
有任务来了,就需要唤醒一个线程去执行
int nThreadPoolPushTask(ThreadPool *pool, struct nTask *task) {
// 加锁保护共享数据
pthread_mutex_lock(&pool->mutex);
// 将任务插入任务链表
LIST_INSERT(task, pool->tasks);
// 通知一个等待的工作者线程有任务可执行
pthread_cond_signal(&pool->cond);
// 解锁
pthread_mutex_unlock(&pool->mutex);
return 0;
}
6.销毁线程池
先把每个线程终止掉,然后广播让它们释放掉内存,最后清空任务队列和工作者队列。
int nThreadPoolDestory(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);
// 解锁
pthread_mutex_unlock(&pool->mutex);
// 清空链表指针
pool->workers = NULL;
pool->tasks = NULL;
return 0;
}
7.任务执行函数
void task_entry(struct nTask *task) {
// 从用户数据中获取任务索引
int idx = *(int *)task->user_data;
printf("idx: %d\n", idx);
// 释放任务相关的内存
free(task->user_data);
free(task);
}
8.主函数
#define THREADPOOL_INIT_COUNT 20 // 初始线程数量
#define TASK_INIT_SIZE 1000 // 初始任务数量
int main(void) {
ThreadPool pool = {0}; // 初始化线程池结构体
// 创建线程池
nThreadPoolCreate(&pool, THREADPOOL_INIT_COUNT);
// 创建并提交多个任务
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, 0, sizeof(struct nTask));
// 设置任务函数和用户数据
task->task_func = task_entry;
// 给任务分配一块堆内存,把任务索引 i 存进去
//让任务执行时能拿到这个唯一索引;
//用堆内存是为了确保参数在任务执行前不被回收,保证数据正确
task->user_data = malloc(sizeof(int));,
*(int*)task->user_data = i;
// 将任务提交到线程池
nThreadPoolPushTask(&pool, task);
}
// 等待用户输入(保持程序运行)
getchar();
// 销毁函数来清理资源
nThreadPoolDestory(&pool, THREADPOOL_INIT_COUNT);
return 0;
}
}
750

被折叠的 条评论
为什么被折叠?



