线程池的原理
线程池适用的场景
当遇到特别耗时的任务,影响其它线程的执行,需要在其他线程异步执行耗时的任务,进而平衡线程资源的开销与cpu核心,做到既能减少线程创建和销毁的开销,又能最大限度的提高 cpu 利用率。
线程池的作用
- 复用线程资源
- 充分利用系统资源
- 异步执行耗时任务
线程池的构成
线程池是一个生产者消费者模型,由生产者线程和线程池(消费者)构成,通过任务队列进行异步解耦,提高程序运行效率,复用线程资源,减少线程创建和销毁的开销。
生产者线程作用是发布任务;任务队列用来存储任务结点,其中包括任务山下文,任务的执行函数等;线程池作用是取出任务,执行任务,在这个过程涉及到线程调度的问题,即从无到有唤醒,从有到无休眠。
线程数量的平衡选择
线程数量的经验公式 (io等待时间+cpu运算时间)*核心数 / CPU运算时间
- io密集型: 2*cpu核心数+有效词盘数
- cpu密集型: cpu核心数
线程池运行原理
线程池作为一个组件,使用线程池的时候,我们需要调用哪些api实现业务。
- 线程池初始化和创建:init 、create
- 抛任务给线程池:push_task
- 销毁:destroy
对于线程池内部实现逻辑如下:
- thread_pool_create
- thread_worker:取出任务,执行任务,任务调度,for遍历,通过mutex + condition
- thread_pool_post:发布任务,对队列操作,改变状态后给线程发信号,此时使用自旋锁,任务发布状态完成,分发给线程
- do_task:做具体的任务
- wait_all_done:等待所有线程都安全退出
- thread_pool_destroy:销毁线程池,具体为:组织产生新任务,通过pthread_cond_broadcast广播的形式,让阻塞在condition 上面的线程唤醒,等待他们退出
线程池的实现
- 执行函数,上下文,任务队列,线程池定义:
// 任务 执行函数。 上下文
typedef struct task_t {
handler_pt func;
void * arg;
} task_t;
// 任务队列
typedef struct task_queue_t {
uint32_t head;
uint32_t tail;
uint32_t count;
task_t *queue;
} task_queue_t;
// 用到的锁 mutex + condition ,会创建一些线程,需要一个任务队列
struct thread_pool_t {
pthread_mutex_t mutex;
pthread_cond_t condition;
pthread_t *threads;
task_queue_t task_queue;
// 标记线程退出
int closed;
int started; // 当前运行的线程数
// 记录线程的数量 队列大小
int thrd_count;
int queue_size;
};
- 创建线程池
thrd_count:由经验公式计算得出,queue_size:由线程数量和线程栈空间大小共同决定
thread_pool_t *
thread_pool_create(int thrd_count, int queue_size) {
thread_pool_t *pool;
if (thrd_count <= 0 || queue_size <= 0) {
return NULL;
}
pool = (thread_pool_t*) malloc(sizeof(*pool));
if (pool == NULL) {
return NULL;
}
pool->thrd_count = 0;
pool->queue_size = queue_size;
pool->task_queue.head = 0;
pool->task_queue.tail = 0;
pool->task_queue.count = 0;
pool->started = pool->closed = 0;
pool->task_queue.queue = (task_t*)malloc(sizeof(task_t)*queue_size);
if (pool->task_queue.queue == NULL) {
// TODO: free pool
return NULL;
}
pool->threads = (pthread_t*) malloc(sizeof(pthread_t) * thrd_count);
if (pool->threads == NULL) {
// TODO: free pool
return NULL;
}
int i = 0;
for (; i < thrd_count; i++) {
if (pthread_create(&(pool->threads[i]), NULL, thread_worker, (void*)pool) != 0) {
// TODO: free pool
return NULL;
}
pool->thrd_count++;
pool->started++;
}
return pool;
}
- 生产者抛出任务
//实现生产者
int
thread_pool_post(thread_pool_t *pool, handler_pt func, void *arg) {
if (pool == NULL || func == NULL) {
return -1;
}
task_queue_t *task_queue = &(pool->task_queue);
if (pthread_mutex_lock(&(pool->mutex)) != 0) {
return -2;
}
if (pool->closed) {
pthread_mutex_unlock(&(pool->mutex));
return -3;
}
if (task_queue->count == pool->queue_size) {
pthread_mutex_unlock(&(pool->mutex));
return -4;
}
//操作队列 从队列尾部添加,头部取出,通过数组,一个头指针一个尾指针
//这里用的互斥锁,用自旋锁效率高一点
task_queue->queue[task_queue->tail].func = func;
task_queue->queue[task_queue->tail].arg = arg;
// tail可能大于数组大小
task_queue->tail = (task_queue->tail + 1) % pool->queue_size;
task_queue->count++;
//用于通知线程条件变量已经满足条件,一定要在改变条件状态以后再给线程发信号
if (pthread_cond_signal(&(pool->condition)) != 0) {
pthread_mutex_unlock(&(pool->mutex));
return -5;
}
pthread_mutex_unlock(&(pool->mutex));
return 0;
}
- 线程池消费任务
// 取出任务, 执行任务, 任务调度
static void *
thread_worker(void *thrd_pool) {
thread_pool_t *pool = (thread_pool_t*)thrd_pool;
task_queue_t *que;
task_t task;
for (;;) {
pthread_mutex_lock(&(pool->mutex));
que = &pool->task_queue;
// while作用防止虚假唤醒 linux pthread_cond_signal
// linux 可能被信号唤醒
// 业务逻辑不严谨,被其他线程抢了该任务
while (que->count == 0 && pool->closed == 0) {
// pthread_mutex_unlock(&(pool->mutex))
// 当前线程就会阻塞在 condition
// ===================================(阻塞)
// 解除阻塞
// pthread_mutex_lock(&(pool->mutex));
pthread_cond_wait(&(pool->condition), &(pool->mutex));
}
if (pool->closed == 1) break;
task = que->queue[que->head];
que->head = (que->head + 1) % pool->queue_size;
que->count--;
pthread_mutex_unlock(&(pool->mutex));
(*(task.func))(task.arg);
}
pool->started--;
pthread_mutex_unlock(&(pool->mutex));
pthread_exit(NULL);
return NULL;
}
- 销毁线程池
int
thread_pool_destroy(thread_pool_t *pool) {
if (pool == NULL) {
return -1;
}
//阻止产生新任务
if (pthread_mutex_lock(&(pool->mutex)) != 0) {
return -2;
}
if (pool->closed) {
thread_pool_free(pool);
return -3;
}
pool->closed = 1;
//广播,让所有阻塞在condition上面的线程都唤醒,等待他们退出
//pthread_cond_signal作用相同,这个是广播形式
if (pthread_cond_broadcast(&(pool->condition)) != 0 ||
pthread_mutex_unlock(&(pool->mutex)) != 0) {
thread_pool_free(pool);
return -4;
}
wait_all_done(pool);
thread_pool_free(pool);
return 0;
}
// 让线程都安全退出
int
wait_all_done(thread_pool_t *pool) {
int i, ret=0;
for (i=0; i < pool->thrd_count; i++) {
if (pthread_join(pool->threads[i], NULL) != 0) {
ret=1;
}
}
return ret;
}
static void
thread_pool_free(thread_pool_t *pool) {
if (pool == NULL || pool->started > 0) {
return;
}
if (pool->threads) {
free(pool->threads);
pool->threads = NULL;
pthread_mutex_lock(&(pool->mutex));
pthread_mutex_destroy(&pool->mutex);
pthread_cond_destroy(&pool->condition);
}
if (pool->task_queue.queue) {
free(pool->task_queue.queue);
pool->task_queue.queue = NULL;
}
free(pool);
}
- 测试代码
设置线程数量为8,队列大小为256,测试不断抛任务给队列的同时进行任务处理,直到队列满了,线程池安全退出。结果表明能进行263个任务处理。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include "thrd_pool.h"
//添加任务就会有nums++
int nums = 0;
//具体任务
int done = 0;
pthread_mutex_t lock;
void do_task(void *arg) {
usleep(10000);
pthread_mutex_lock(&lock);
done++;
printf("doing %d task\n", done);
pthread_mutex_unlock(&lock);
}
int main(int argc, char **argv) {
//默认值
int threads = 8;
int queue_size = 256;
if (argc == 2) {
threads = atoi(argv[1]);
if (threads <= 0) {
printf("threads number error: %d\n", threads);
return 1;
}
} else if (argc > 2) {
threads = atoi(argv[1]);
queue_size = atoi(argv[1]);
if (threads <= 0 || queue_size <= 0) {
printf("threads number or queue size error: %d,%d\n", threads, queue_size);
return 1;
}
}
thread_pool_t *pool = thread_pool_create(threads, queue_size);
if (pool == NULL) {
printf("thread pool create error!\n");
return 1;
}
//添加任务,
while (thread_pool_post(pool, &do_task, NULL) == 0) {
pthread_mutex_lock(&lock);
nums++;
pthread_mutex_unlock(&lock);
}
printf("add %d tasks\n", nums);
wait_all_done(pool);
printf("did %d tasks\n", done);
thread_pool_destroy(pool);
return 0;
}
- 测试结果
线程池在具体场景的应用
redis为什么需要线程池
redis不是cpu密集型,因为内存的操作很快,而瓶颈来自于网络io,所以redis 选择了单线程的 I/O 多路复用来实现它的核心网络模型。
当客户端写大量的io请求(写日志)给服务器,redis的读压力很大,客户端向服务器读大量的io请求,redis的写压力就很大,此时就需要线程池进行处理,主线程首先收集所有读请求(read、decode)到一个队列(clients_pending_read)中,把每个事件负载均衡地分配给多个io线程中,主线程将解析后的任务做compute操作,写请求(encode、send)同理。