目录
线程池是一个用于管理并发执行的线程的集合,可以有效地控制和优化多线程程序的性能。在详细讨论线程池之前,我们首先了解它的基本概念和原理。
1、基本概念
线程池主要用于管理一组预先创建的线程,它们在一个共享的工作队列中等待任务。使用线程池可以避免在每个任务执行时创建和销毁线程的高成本,从而提高效率和响应速度。
2、线程池的关键特点
- 线程复用:线程一旦创建,可以被多个任务复用,避免了频繁创建和销毁线程的开销。
- 资源控制:线程池可以限制系统中线程的最大数量,防止过多线程耗尽系统资源。
- 提高响应速度:任务可以不需要等待线程创建就立即执行。
- 更好的线程管理:提供了一种限制和管理线程的手段,比如调度策略、线程优先级设置等。
3、线程池的工作流程
- 任务提交:应用程序将执行的任务提交到线程池。
- 任务存储:线程池维护一个待执行任务的队列。
- 任务执行:空闲的线程从队列中取出任务并执行。
- 任务完成:线程完成任务后,返回线程池等待下一个任务。
4、线程池的主要组件
- 线程池管理器:负责管理线程池的创建、销毁、以及线程数目的调整。
- 工作队列:用于存放待处理的任务。
- 工作者线程:线程池中的线程,负责执行队列中的任务。
5、常见的线程池类型
- 固定大小的线程池:拥有固定数量的线程,适用于负载比较均匀的情况。
- 可缓存的线程池:线程数不固定,可以根据需求自动的增加新线程,适用于任务执行时间短的场景。
- 单线程化的线程池:只有一个线程,所有任务按顺序执行,适用于需要顺序执行任务的场景。
- 定时任务线程池:可以在指定时间执行任务或者周期性执行任务。
6、线程池的应用场景
线程池适用于服务器处理客户端的并发请求、大数据平台上进行数据分析、图形界面应用程序中进行后台任务处理等场景。
7、根据IO/CPU密集型分配线程数量
在设计和配置线程池时,理解 I/O 密集型和 CPU 密集型任务的特点是非常重要的,因为它们对资源的需求和使用模式有很大的不同。这会直接影响你应该如何设定线程池中的线程数量以及其他相关参数。
7.1、I/O 密集型任务
I/O 密集型任务指的是那些大部分时间都在等待外部操作的任务,比如网络请求、文件读写或数据库交互等。这些任务的特点是CPU计算较少,而更多时间花在等待I/O操作的完成上。
线程数量设置:2n
- 对于 I/O 密集型任务,线程池可以配置得相对较大,因为线程大部分时间处于等待状态,不会占用太多CPU资源。你可以设置的线程数比CPU核心数多得多,通常可以设置为 CPU 核心数的两倍或更多,具体数值可以根据实际的 I/O 等待时间和响应要求来调整。
7.2、CPU 密集型任务
CPU 密集型任务是指那些需要大量计算,持续占用CPU资源来处理的任务,例如图像处理、复杂计算或大数据处理等。
线程数量设置:n+1
- 对于 CPU 密集型任务,线程池的大小通常应该设置接近于处理器的核心数量。一般推荐设置为 CPU 核心数,或者核心数加一,这样可以充分利用CPU资源,同时避免过多的线程切换带来的开销。
- 在有超线程技术的系统中,可以将线程数设置为物理核心的两倍。
8、使用注意事项
- 合理配置线程数量:线程数太少可能导致处理能力不足,太多则可能导致资源浪费。
- 注意任务的分配:避免大量长时间运行的任务占用所有线程,导致短任务无法及时处理。
- 资源同步:注意多线程之间的数据同步和互斥,避免数据冲突和竞态条件。
线程池是并发编程中的一个非常有用的工具,通过合理的使用线程池,可以大幅提高应用程序的性能和效率。
9、代码实践
9.1、核心结构
9.1.1、task_s结构体(任务结构)
typedef struct task_s {
void *next; // 指向下一个任务的指针
handler_pt func; // 任务处理函数
void *arg; // 传递给处理函数的参数
} task_t;
解释:
void *next
:这是一个通用指针,用于指向下一个任务。它将任务链接在一起,形成一个链表结构。handler_pt func
:这是一个函数指针,用于指向任务处理函数。handler_pt
是一个用户定义的函数类型,表示处理任务的函数原型。每个任务有一个对应的处理函数。void *arg
:这是传递给func
处理函数的参数,类型为void*
,以便可以传递任意类型的数据。
用法:
当一个任务被提交到线程池时,它会被封装成 task_t
结构体,存储任务的执行逻辑(函数)和相关的参数,并通过 next
指针链接到任务队列中。
9.1.2、task_queue_s
结构体 (任务队列)
typedef struct task_queue_s {
void *head; // 指向任务队列头部(第一个任务)
void **tail; // 指向任务队列尾部(最后一个任务的 next 指针)
int block; // 标记是否阻塞(用于控制任务队列的状态)
spinlock_t lock; // 自旋锁,用于保护任务队列的并发访问
pthread_mutex_t mutex; // 互斥锁,用于任务队列的同步
pthread_cond_t cond; // 条件变量,用于实现任务队列的阻塞等待
} task_queue_t;
解释:
void *head
:这是指向任务队列中第一个任务的指针。任务队列是一个链表结构,第一个任务就是队列的头。void **tail
:这是指向任务队列中最后一个任务的next
指针地址的指针。通过它可以方便地将新任务追加到队列末尾。即*tail
是队列最后一个任务的next
,通常指向NULL
。int block
:这个标志变量用于表示任务队列是否处于阻塞状态。可以用于控制任务提交和获取的行为。spinlock_t lock
:自旋锁,用于对任务队列进行锁定,确保在多线程环境下对队列的并发访问是安全的。与互斥锁不同,自旋锁不引起线程睡眠,而是循环等待锁释放。pthread_mutex_t mutex
:互斥锁,用于在多个线程之间同步访问共享资源。在任务队列中,它通常用于在插入或删除任务时保护队列。pthread_cond_t cond
:条件变量,用于线程之间的等待与通知机制。例如,当任务队列为空时,线程可以等待新任务被添加,当有新任务时,通过条件变量通知等待的线程。
用法:
任务队列用于存储线程池中待执行的任务。线程池中的工作线程会从任务队列中取任务执行。当队列为空时,工作线程会通过条件变量等待新任务的到来。
9.1.3、thrdpool_s
结构体 (线程池结构)
struct thrdpool_s {
task_queue_t *task_queue; // 指向任务队列的指针
atomic_int quit; // 原子变量 quit,用于标记线程池是否正在关闭
int thrd_count; // 线程池中的线程数量
pthread_t *threads; // 线程数组,存储线程池中的所有线程
};
解释:
task_queue_t *task_queue
:指向任务队列的指针。每个线程池都维护一个任务队列,任务会被添加到这个队列中等待线程处理。atomic_int quit
:这是一个原子变量,标志线程池是否已经关闭。使用原子变量可以确保在多线程环境中进行安全的读写操作,避免数据竞争。当quit
为 1 时,说明线程池正在关闭,不再接受新的任务。int thrd_count
:线程池中线程的数量,表示有多少个工作线程会从任务队列中取任务并执行。pthread_t *threads
:线程数组,用于存储线程池中的所有线程。每个线程都会执行某个函数,循环从任务队列中获取任务并处理。
用法:
线程池结构体维护了线程池的核心状态:任务队列、线程数组以及线程池是否正在关闭等信息。线程池的主要任务是管理多个工作线程的调度和任务分发。
9.1.4、总结
task_t
结构体代表一个任务,包括任务处理函数和相关参数。task_queue_t
结构体是任务队列,负责存储待执行的任务并管理线程同步。thrdpool_s
结构体是线程池的核心,包含任务队列、线程数组和线程池的状态信息。
9.2、池内任务队列函数
9.2.1、任务队列创建
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;
}
9.2.2、任务队列添加任务
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 */ = link;
queue->tail = link;
spinlock_unlock(&queue->lock);
pthread_cond_signal(&queue->cond);
}
这里二级指针详细说下:
void **link = (void**)task;
*link = NULL;
task
是一个void*
类型的指针,指向某个任务结构体。link
是一个void**
类型的指针,指向一个void*
类型的指针。
理解*link:
*link 的意思是“解引用 link 指针”,即获取 link 所指向的那个 void* 指针的值。具体来说:
link
是一个指向void*
的指针(void**
类型)。*link
是link
所指向的那个void*
指针的值。
*link = NULL; 的含义
*link = NULL; 时,你是在修改 link 指向的那个 void* 指针的值,将它设置为 NULL。换句话说:
link
指向任务结构体的第一个成员,这个成员是一个指针(void*
类型),通常用于指向链表中的下一个任务节点。*link
就是这个成员本身(即void*
类型的指针)。*link = NULL;
将这个指针设置为NULL
,表示该任务节点目前没有下一个任务节点。
强转为二级指针后,首地址不变,解引用后还是指向首地址但是范围变了,指向结构体第一个next指针。
*queue->tail /* 等价于 queue->tail->next */ = link;
queue->tail = link;
*queue->tail /* 等价于 queue->tail->next */ = link;
:这一行代码将新任务添加到队列的末尾。queue->tail
是指向队列中最后一个任务的指针,*queue->tail
实际上是最后一个任务的next
指针。将link
赋值给*queue->tail
后,新任务被链入队列。queue->tail = link;
:更新队列的尾指针queue->tail
,使其指向新添加的任务。此时,新任务成为队列中的最后一个任务。
9.2.3、任务队列弹出任务
具体步骤涉及对队列头的访问、移除操作以及线程安全的锁机制。
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;
}
其中:
void **link = (void**)task;
queue->head = *link;
- 将
task
(头部任务)的地址强制转换为void**
类型的指针link
。这一步实际上将task_t
的地址看作是指向一个void*
的指针。 - 然后,将队列的
head
更新为*link
,即task
中的下一个任务。这样做的目的是从队列中移除当前头部任务,并将第二个任务设置为新的head
。
9.2.3、任务队列获取任务
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;
}
while循环的目的---虚假唤醒:pthread_cond_wait
可能由于系统原因或竞争条件被虚假唤醒,即使没有任务被添加。因此在进入循环后,任务仍需要再次检查队列是否有任务可用。
9.2.4、销毁任务队列
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);
}
9.2、池内函数
9.2.1、工作线程函数
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;
}
实现了一个线程池中的工作线程函数 __thrdpool_worker
,它会不断从任务队列中取任务并执行,直到线程池的退出标志被设置。
9.2.2、创建线程函数
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;
}
给定一个线程池 thrdpool_t
对象和线程数量 thrd_count
,函数为线程池分配内存,并创建相应数量的线程。
- 尝试创建指定数量的线程,线程池的线程句柄被存储在
pool->threads
数组中。 - 每个线程运行函数
__thrdpool_worker
,并且线程池的thrd_count
记录实际创建的线程数量。 - 如果任意线程创建失败,已创建的线程会被终止,并且已分配的内存也会被释放,确保不留资源泄漏。
9.2.3、终止线程函数
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);
}
}
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);
}
使用 atomic_store
来设置一个退出标志,然后通过 pthread_join
来等待所有线程的结束。
9.3、接口函数
9.3.1、创建线程池
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;
}
线程池的创建函数 thrdpool_create
,通过为线程池分配内存、创建任务队列和线程,并初始化必要的变量来实现线程池的初始化。先分配内存并初始化任务队列,设置线程池的状态,然后尝试创建指定数量的线程。如果线程创建成功,则返回一个指向线程池的指针;如果失败,则清理已分配的资源并返回 NULL
。
9.3.2、任务提交
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;
}
向线程池提交一个新的任务,并在所有线程完成任务后清理线程池。
参数:
pool
: 线程池对象的指针,类型为thrdpool_t
。func
: 任务处理函数,类型为handler_pt
,可以理解为函数指针。arg
: 传递给处理函数的参数,类型为void*
,表示可以传递任何类型的数据。
功能:
-
检查是否退出:通过
atomic_load(&pool->quit)
检查线程池的退出状态。如果pool->quit
为 1,说明线程池正在关闭,无法再接受任务,此时返回-1
表示任务提交失败。 -
分配任务:使用
malloc
为一个新的任务task
分配内存。如果分配失败,返回-1
。 -
设置任务:将传入的任务处理函数
func
和参数arg
赋值给新创建的任务task
。 -
添加任务到队列:调用
__add_task(pool->task_queue, task)
,将任务添加到线程池的任务队列中。这通常是一个内部函数,用于将任务加入一个任务队列中,等待线程池的线程处理。 -
返回 0 表示成功:如果任务成功提交,则返回 0,表示操作成功。
9.3.3、等待函数
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);
}
参数:
pool
: 线程池对象的指针,类型为thrdpool_t
。
功能:
-
等待所有线程完成工作:通过
pthread_join
函数等待线程池中的每个线程(pool->threads[i]
)完成任务。pthread_join
是一个阻塞操作,它等待线程执行结束。NULL
表示不关心线程的返回值。 -
销毁任务队列:调用
__taskqueue_destroy(pool->task_queue)
销毁任务队列,释放队列中所有未处理的任务。这是线程池清理的一部分,确保不再有任务留在队列中。 -
释放资源:
- 释放线程数组
pool->threads
。 - 释放线程池结构
pool
自身。
- 释放线程数组
9.3.4、终止函数
void thrdpool_terminate(thrdpool_t * pool) {
atomic_store(&pool->quit, 1);
__nonblock(pool->task_queue);
}
通过设置线程池的一个标志位 quit
,并调用函数 __nonblock
来实现对任务队列的处理。