目录
[1. 核心数据结构:先把架子搭起来](#1. 核心数据结构:先把架子搭起来)
[2. 三步实现法:从能用→好用→优雅](#2. 三步实现法:从能用→好用→优雅)
[3. 测试用例:](#3. 测试用例:)
[4. 常见难点与解决方案](#4. 常见难点与解决方案)
[5. 扩展思考:线程池还能怎么玩?](#5. 扩展思考:线程池还能怎么玩?)
引言
兄弟们,学完Linux线程(pthread)、互斥锁、条件变量,是不是感觉理论知识都会了,但真要动手写个并发程序,还是有点虚?别慌,今天咱们就来干一件最实在的事:手撕一个线程池。
为什么要手撕线程池?因为它是把"生产者-消费者模型"、"线程同步"、"资源管理"这些硬核知识点串起来的最佳实践。你写一遍,胜过看十遍书。而且,面试官最爱问这个。
话不多说,直接开干!我们的目标是:从零开始,写出一个能用的C语言线程池,并逐步让它变强。
1. 核心数据结构:先把架子搭起来
一切代码的基础是数据结构(先描述再组织 )。线程池的核心就两个结构体:任务(
task_t)和线程池本身(thread_pool_t)。
cpp
// task.h
#ifndef TASK_H
#define TASK_H
// 任务结构体
typedef struct task {
void (*function)(void* arg); // 任务函数指针
void* arg; // 函数参数
struct task* next; // 链表指针,用于构建任务队列
} task_t;
#endif
cpp
// thread_pool.h
#ifndef THREAD_POOL_H
#define THREAD_POOL_H
#include <pthread.h>
#include "task.h"
// 线程池结构体
typedef struct thread_pool {
task_t* task_queue; // 任务队列头指针(链表实现)
pthread_mutex_t queue_mutex; // 保护任务队列的互斥锁
pthread_cond_t queue_cond; // 条件变量,用于线程等待/唤醒
pthread_t* threads; // 工作线程ID数组
int thread_count; // 线程池中线程数量
int shutdown; // 关闭标志:0-运行,1-关闭
} thread_pool_t;
// 函数声明
thread_pool_t* thread_pool_create(int thread_count);
void thread_pool_destroy(thread_pool_t* pool);
void thread_pool_submit(thread_pool_t* pool, void (*func)(void*), void* arg);
#endif
关键点说明:
- 任务队列 :用单向链表实现,简单高效。
task_t就是链表节点。- 互斥锁
queue_mutex:保护task_queue这个共享资源,防止多个线程同时操作导致数据错乱。- 条件变量
queue_cond:当任务队列为空时,工作线程通过它进入等待状态;当有新任务加入时,通过它唤醒一个等待的线程。shutdown标志:优雅关闭线程池的关键。
2. 三步实现法:从能用→好用→优雅
第一步:基础版本(固定线程数,能跑就行)
这是最核心的部分,我们直接上完整代码。
cpp
// thread_pool.c
#include "thread_pool.h"
#include <stdlib.h>
#include <stdio.h>
// 工作线程执行的函数
static void* worker(void* arg) {
thread_pool_t* pool = (thread_pool_t*)arg;
while (1) {
pthread_mutex_lock(&pool->queue_mutex);
// 条件变量等待:队列为空且未关闭时等待
while (pool->task_queue == NULL && !pool->shutdown) {
pthread_cond_wait(&pool->queue_cond, &pool->queue_mutex);
}
// 如果关闭且任务队列为空,线程退出
if (pool->shutdown && pool->task_queue == NULL) {
pthread_mutex_unlock(&pool->queue_mutex);
pthread_exit(NULL);
}
// 取出队列头部的任务
task_t* task = pool->task_queue;
pool->task_queue = task->next;
pthread_mutex_unlock(&pool->queue_mutex);
// ️ 执行任务时,已经释放了锁!这是关键设计
task->function(task->arg);
free(task);
}
return NULL;
}
// 创建线程池
thread_pool_t* thread_pool_create(int thread_count) {
thread_pool_t* pool = (thread_pool_t*)malloc(sizeof(thread_pool_t));
pool->task_queue = NULL;
pool->thread_count = thread_count;
pool->shutdown = 0;
pthread_mutex_init(&pool->queue_mutex, NULL);
pthread_cond_init(&pool->queue_cond, NULL);
pool->threads = (pthread_t*)malloc(thread_count * sizeof(pthread_t));
for (int i = 0; i < thread_count; i++) {
pthread_create(&pool->threads[i], NULL, worker, pool);
}
return pool;
}
// 提交任务
void thread_pool_submit(thread_pool_t* pool, void (*func)(void*), void* arg) {
task_t* new_task = (task_t*)malloc(sizeof(task_t));
new_task->function = func;
new_task->arg = arg;
new_task->next = NULL;
pthread_mutex_lock(&pool->queue_mutex);
// 将新任务添加到队列尾部
if (pool->task_queue == NULL) {
pool->task_queue = new_task;
} else {
task_t* current = pool->task_queue;
while (current->next != NULL) {
current = current->next;
}
current->next = new_task;
}
// 唤醒一个等待的工作线程
pthread_cond_signal(&pool->queue_cond);
pthread_mutex_unlock(&pool->queue_mutex);
}
// 销毁线程池
void thread_pool_destroy(thread_pool_t* pool) {
pthread_mutex_lock(&pool->queue_mutex);
pool->shutdown = 1;
// 广播唤醒所有线程,让它们检查shutdown标志并退出
pthread_cond_broadcast(&pool->queue_cond);
pthread_mutex_unlock(&pool->queue_mutex);
// 等待所有线程结束
for (int i = 0; i < pool->thread_count; i++) {
pthread_join(pool->threads[i], NULL);
}
// 清理剩余任务
task_t* current = pool->task_queue;
while (current != NULL) {
task_t* next = current->next;
free(current);
current = next;
}
free(pool->threads);
pthread_mutex_destroy(&pool->queue_mutex);
pthread_cond_destroy(&pool->queue_cond);
free(pool);
}
基础版本核心逻辑:
worker函数:死循环,加锁检查队列,无任务则等待,有任务则取出执行。thread_pool_submit:加锁,将任务添加到队列尾部,解锁,并唤醒一个线程。- 关键设计:任务执行在锁外,避免长时间持有锁导致其他线程饥饿。
第二步:添加动态扩容(进阶)
基础版线程数是固定的。如果任务突然暴增,固定线程数可能处理不过来。我们可以添加动态扩容功能。
cpp
// 在 thread_pool_t 结构体中添加字段
typedef struct thread_pool {
// ... 其他字段
int min_threads; // 最小线程数
int max_threads; // 最大线程数
int idle_threads; // 当前空闲线程数
int active_threads; // 当前活跃线程数
// ... 其他字段
} thread_pool_t;
// 管理线程函数(在单独的线程中运行)
static void* manager(void* arg) {
thread_pool_t* pool = (thread_pool_t*)arg;
while (!pool->shutdown) {
sleep(2); // 每隔2秒检查一次
pthread_mutex_lock(&pool->queue_mutex);
int queue_size = get_queue_size(pool); // 需要实现一个获取队列长度的函数
int idle = pool->idle_threads;
int total = pool->active_threads;
pthread_mutex_unlock(&pool->queue_mutex);
// 扩容条件:任务队列长度 > 空闲线程数,且未达到最大线程数
if (queue_size > idle && total < pool->max_threads) {
int add_count = (queue_size - idle) < (pool->max_threads - total) ?
(queue_size - idle) : (pool->max_threads - total);
for (int i = 0; i < add_count; i++) {
pthread_t new_thread;
pthread_create(&new_thread, NULL, worker, pool);
// 需要将新线程ID保存到动态数组中(代码略)
}
printf("[Manager] Added %d threads. Total: %d\n", add_count, total + add_count);
}
// 缩容条件:空闲线程数 > 任务队列长度 * 2,且大于最小线程数
if (idle > queue_size * 2 && total > pool->min_threads) {
// 发送"自杀"信号给空闲线程(实现方式:设置标志位或使用额外的条件变量)
// 这里简化处理,实际需要更复杂的机制
printf("[Manager] Ready to reduce threads.\n");
}
}
return NULL;
}
注意:动态扩容/缩容的实现比较复杂,需要处理线程ID的动态管理、空闲线程的精确计数、以及如何让空闲线程安全退出等问题。这里只展示核心思路,完整实现留作你的练习。
第三步:优化设计(无锁队列、优先级、监控接口)
对于追求极致性能的场景,可以引入更高级的优化。
优化1:无锁队列
使用CAS(Compare-And-Swap)原子操作实现无锁队列,避免锁竞争,提高性能。但这会大大增加代码复杂度,且容易出错。对于大多数场景,互斥锁已经足够。(这里不做演示)
优化2:任务优先级将任务队列改为优先队列(如使用堆或有序链表),让高优先级的任务先被执行。
cpp
// 在 task_t 中添加优先级字段
typedef struct task {
void (*function)(void* arg);
void* arg;
int priority; // 优先级,数值越大优先级越高
struct task* next;
} task_t;
// 提交任务时,按优先级插入到队列中
void thread_pool_submit_priority(thread_pool_t* pool, void (*func)(void*), void* arg, int priority) {
task_t* new_task = (task_t*)malloc(sizeof(task_t));
new_task->function = func;
new_task->arg = arg;
new_task->priority = priority;
new_task->next = NULL;
pthread_mutex_lock(&pool->queue_mutex);
// 按优先级从高到低插入
task_t** p = &pool->task_queue;
while (*p != NULL && (*p)->priority >= priority) {
p = &(*p)->next;
}
new_task->next = *p;
*p = new_task;
pthread_cond_signal(&pool->queue_cond);
pthread_mutex_unlock(&pool->queue_mutex);
}
优化3:监控接口
提供接口查询线程池状态,如当前任务队列长度、活跃线程数、已完成任务数等,方便调优。
cpp
// 获取线程池状态
typedef struct pool_status {
int task_queue_size;
int active_threads;
int total_tasks_completed;
} pool_status_t;
pool_status_t thread_pool_get_status(thread_pool_t* pool) {
pool_status_t status;
pthread_mutex_lock(&pool->queue_mutex);
status.task_queue_size = get_queue_size(pool);
status.active_threads = pool->active_threads;
status.total_tasks_completed = pool->completed_tasks;
pthread_mutex_unlock(&pool->queue_mutex);
return status;
}
3. 测试用例:
cpp
// test.c
#include "thread_pool.h"
#include <stdio.h>
#include <unistd.h>
// 一个简单的任务函数:打印任务ID和线程ID
void my_task(void* arg) {
int task_id = *(int*)arg;
printf("Task %d is being executed by thread %lu\n", task_id, (unsigned long)pthread_self());
usleep(100000); // 模拟任务执行耗时 100ms
free(arg); // 释放参数内存
}
// 基础测试
void test_basic() {
printf("=== Test Basic ===\n");
thread_pool_t* pool = thread_pool_create(4); // 创建4个线程的线程池
for (int i = 0; i < 10; i++) {
int* arg = malloc(sizeof(int));
*arg = i;
thread_pool_submit(pool, my_task, arg);
}
sleep(2); // 等待任务执行完毕
thread_pool_destroy(pool);
printf("=== Test Basic Done ===\n");
}
int main() {
test_basic();
return 0;
}
编译运行:
cppgcc -o test test.c thread_pool.c -lpthread -I. ./test预期输出(线程ID和顺序可能不同):
4. 常见难点与解决方案
| 难点 | 现象 | 解决方案 |
|---|---|---|
| 虚假唤醒 | 线程被唤醒但队列仍为空,导致访问空指针 | 使用 while 循环检查 条件,而非 if |
| 死锁 | 程序卡死,无响应 | 确保加锁和解锁成对出现;避免在持有锁时调用可能阻塞的函数;使用RAII思想 |
| 任务执行耗时 | 一个耗时任务阻塞了线程,导致其他任务无法被处理 | 将任务执行放在锁外;考虑为长时间任务单独开线程 |
| 优雅关闭 | 强制关闭导致任务丢失或线程资源泄漏 | 设置 shutdown 标志;广播唤醒所有线程;pthread_join 等待线程结束 |
关键知识点:
| 知识点 | 一句话总结 |
|---|---|
| 互斥锁 | 保护共享资源(任务队列)的原子访问 |
| 条件变量 | 实现线程的等待与唤醒,避免忙等待 |
| 任务队列 | 生产者和消费者之间的缓冲区 |
| 锁外执行 | 任务执行时释放锁,提高并发度 |
| 优雅关闭 | 设置标志 + 广播唤醒 + 等待线程退出 |
5. 扩展思考:线程池还能怎么玩?
- 任务窃取(Work Stealing):每个线程有自己的任务队列,当某个线程空闲时,可以从其他线程的队列尾部"偷"任务来执行,提高负载均衡。Go语言的GMP模型就采用了类似思想。
- CPU密集型 vs IO密集型适配 :
- CPU密集型 :线程数通常设置为
CPU核心数 + 1,避免过多线程切换开销。- IO密集型 :线程数可以设置得更多,如
CPU核心数 * 2或更高,因为线程经常在等待IO。- 网络编程集成:将线程池与epoll结合,主线程负责监听和接收新连接,然后将连接处理任务提交给线程池,实现高性能Reactor模型。
