为什么要使用线程池?
线程是处理器调度的基本单位。我们会为每一个请求都独立创建一个线程,而操作系统创建线程、切换线程状态、结束线程都要使用CPU进行调度。消耗了很大资源,但使用线程池能够更好对线程进行管理、复用等。
线程池的组成部分
- 线程管理控制器:用于创建管理线程池。
- 工作线程:线程池中实际执行任务的线程。在初始化线程时会预先创建好固定数目的线程在池中,这些初始化的线程一般处于空闲状态。
- 任务接口:每个任务必须实现的接口。
- 任务队列:用于存放没有被处理的任务。
线程池的流程
- 首先创建线程池,并创建若干个线程并置于空闲状态
- 如果任务队列不为空,任务到达时,从线程池中取出线程处理任务事件,然后通知可以与新的任务添加进来
- 如果任务队列不为满,向任务队列中添加任务,并通知线程池,需要线程来处理任务。
- 销毁线程池
代码如下:
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#define true 1
#define false 0
typedef struct
{
void *(*function)(void *); // 函数指针,回调函数
void *arg; // 上面函数的参数
}threadpool_task_t; // 各子线程任务结构体
struct threadpool_t
{
pthread_mutex_t lock; // 线程池的锁(锁住本结构体)
pthread_mutex_t thread_counter; // 记录忙状态线程个数的锁
pthread_cond_t queue_not_full; // 任务队列满时,线程池中的线程阻塞,等待此条件变量
pthread_cond_t queue_not_empty; // 任务队列不为空时,通知等待任务的线程
pthread_t *threads; // 线程池(存放的是每个线程的id)
pthread_t adjust_tid; // 管理者线程(管理线程池)
threadpool_task_t *task_queue; // 任务队列(数组首地址)
int min_thr_num; // 线程池最小线程数
int max_thr_num; // 线程池最大线程数
int busy_thr_num; // 忙状态线程数
int live_thr_num; // 当前存活的线程数
int wait_exit_thr_num; // 要销毁的线程数
int queue_front; // 任务队列的队头
int queue_rear; // 任务队列的队尾
int queue_size; // 任务队列的实际任务数
int queue_max_size; // 任务队列可容纳任务数的上限
int shutdown; // 标志位,线程池使用状态,true或者false
};
void threadpool_free(threadpool_t *pool);
void *threadpool_thread(void *threadpool);
void *adjust_thread(void *threadpool);
// 创建线程池并做一些初始化的工作
threadpool_t *pthreadpool_create(int min_thr_num, int max_thr_num, int queue_max_size)
{
int i;
threadpool_t *pool = NULL;
do
{
if ( (pool = (threadpool_t *)malloc(sizeof(threadpool_t))) == NULL )
{
printf("malloc threadpool fail\n");
break;
}
// 初始化一些基本参数
pool->min_thr_num = min_thr_num;
pool->max_thr_num = max_thr_num;
pool->busy_thr_num = 0;
pool->live_thr_num = min_thr_num;
pool->wait_exit_thr_num = 0;
pool->queue_size = 0;
pool->queue_max_size = queue_max_size;
pool->queue_front = 0;
pool->queue_rear = 0;
pool->shutdown = false;
// 初始化线程池,开辟数组空间
pool->threads = (pthread_t *)malloc(sizeof(pthread_t) * max_thr_num);
if (pool->threads == NULL)
{
printf("malloc threads fail\n");
break;
}
memset(pool->threads, 0, sizeof(pthread_t) * max_thr_num);
// 初始化任务队列
pool->task_queue = (threadpool_task_t *)malloc(sizeof(threadpool_task_t) * queue_max_size);
if (pool->task_queue == NULL)
{
printf("malloc task_queue fail\n");
break;
}
// 初始化锁和条件变量
if (pthread_mutex_init(&(pool->lock), NULL) != 0
|| pthread_mutex_init(&(pool->thread_counter), NULL) != 0
|| pthread_cond_init(&(pool->queue_not_empty), NULL) != 0
|| pthread_cond_init(&(pool->queue_not_full), NULL) != 0)
{
printf("init lock or cond fail\n");
break;
}
// 初始化线程池和管理者线程
for (i = 0; i < max_thr_num; ++i)
{
pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void *)pool);
printf("start thread %lu ...\n", pool->threads[i]);
}
pthread_create(&(pool->adjust_tid), NULL, adjust_thread, (void *)pool);
return pool;
}while(0);
// 前面代码调用失败时,释放pool的空间
threadpool_free(pool);
}
// 线程池中的每个线程或者终止或者处理任务
void *threadpool_thread(void *threadpool)
{
threadpool_t *pool = (threadpool_t *)threadpool;
threadpool_task_t task;
while (true)
{
pthread_mutex_lock(&(pool->lock));
// 如果任务队列为空,则调用wait阻塞在等待条件上,当wait被调用时说明有任务,则退出while
while ( (pool->queue_size == 0) && (!pool->shutdown) )
{
printf("thread %lu is waitting\n", pthread_self());
// 等待工作队列不为空的条件
pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock));
// 任务队列为空了,已经没有任务可以执行了,让所有线程自动终止
if (pool->wait_exit_thr_num > 0)
{
pool->wait_exit_thr_num--;
if (pool->live_thr_num > pool->min_thr_num)
{
printf("thread %lu is exitting\n", pthread_self());
pool->live_thr_num--;
pthread_mutex_unlock(&(pool->lock));
pthread_exit(NULL);
}
}
}
if (pool->shutdown)
{
pthread_mutex_unlock(&(pool->lock));
printf("thread %lu is exitting\n", pthread_self());
pthread_detach(pthread_self());
pthread_exit(NULL);
}
// 从任务队列中取任务(队头)
task.function = pool->task_queue[pool->queue_front].function;
task.arg = pool->task_queue[pool->queue_front].arg;
// 队头元素向后移动
pool->queue_front = (pool->queue_front + 1) % pool->queue_max_size;
pool->queue_front++;
// 通知可以有新的任务添加进来
pthread_cond_signal(&(pool->queue_not_full));
pthread_mutex_unlock(&(pool->lock));
// 正在处理任务,忙状态线程数加1
printf("thread %lu is working\n", pthread_self());
pthread_mutex_lock(&(pool->thread_counter));
pool->busy_thr_num++;
pthread_mutex_unlock(&(pool->thread_counter));
// 执行回调函数去处理任务
(*(task.function))(task.arg);
// 任务处理完毕,忙状态线程数减1
printf("thread %lu is end working\n", pthread_self());
pthread_mutex_lock(&(pool->thread_counter));
pool->busy_thr_num--;
pthread_mutex_unlock(&(pool->thread_counter));
}
pthread_exit(NULL);
}
// 管理者线程
void *adjust_thread(void *threadpool)
{
int i;
threadpool_t *pool = (threadpool_t *)threadpool;
while (!pool->shutdown)
{
// 每隔10秒对当前线程池进行管理
sleep(10);
// 通过加锁和解锁访问数据
pthread_mutex_lock(&(pool->lock));
int queue_size = pool->queue_size;
int live_thr_num = pool->live_thr_num;
pthread_mutex_unlock(&(pool->lock));
pthread_mutex_lock(&(pool->thread_counter));
int busy_thr_num = pool->busy_thr_num;
pthread_mutex_unlock(&(pool->thread_counter));
// 线程池扩容和瘦身
}
}
// 向任务队列中添任务并通知线程池去处理任务
int threadpool_add(threadpool_t *pool, void *(*function)(void *arg), void *arg)
{
pthread_mutex_lock(&(pool->lock));
while ( (pool->queue_size == pool->queue_max_size) && (!pool->shutdown) )
{
pthread_cond_wait(&(pool->queue_not_full), &(pool->lock));
}
// 如果线程池需要关闭了, 通知线程池中的线程自动终止
if (pool->shutdown)
{
pthread_cond_signal(&(pool->queue_not_empty));
pthread_mutex_unlock(&(pool->lock));
return 0;
}
if (pool->task_queue[pool->queue_rear].arg != NULL)
{
pool->task_queue[pool->queue_rear].arg == NULL;
}
pool->task_queue[pool->queue_rear].function = function;
pool->task_queue[pool->queue_rear].arg = arg;
pool->queue_rear = (pool->queue_rear + 1) % pool->queue_max_size;
pool->queue_rear++;
// 添加完任务后,任务队列不为空,通知线程池中的一个线程去处理任务
pthread_cond_signal(&(pool->queue_not_empty));
pthread_mutex_unlock(&(pool->lock));
}
// 释放所有的资源
void threadpool_free(threadpool_t *pool)
{
if (pool == NULL)
{
return;
}
if (pool->task_queue)
{
free(pool->task_queue);
}
// 释放和线程池相关的资源
if (pool->threads)
{
free(pool->threads);
pthread_mutex_lock(&(pool->lock));
pthread_mutex_destroy(&(pool->lock));
pthread_mutex_lock(&(pool->thread_counter));
pthread_mutex_destroy(&(pool->thread_counter));
pthread_cond_destroy(&(pool->queue_not_empty));
pthread_cond_destroy(&(pool->queue_not_full));
}
free(pool);
return;
}
// 销毁线程池(通知线程池中的线程自我终止)
void threadpool_destroy(threadpool_t *pool)
{
int i;
if (pool == NULL)
{
return;
}
pool->shutdown = true;
// 销毁管理者线程
pthread_detach(pool->adjust_tid);
for (i = 0; i < pool->live_thr_num; ++i)
{
// 通知所有空闲线程
pthread_cond_signal(&(pool->queue_not_empty));
}
for (i = 0; i < pool->live_thr_num; ++i)
{
// 回收空闲线程
pthread_detach(pool->threads[i]);
}
threadpool_free(pool);
return;
}
// 实际需要处理的任务
void *process(void *arg)
{
sleep(1);
printf("hello\n");
return NULL;
}
int main(void)
{
// 创建线程池,最小3个线程,最大100个线程,任务队列最大容量为100
threadpool_t *thp = pthreadpool_create(3, 100, 100);
printf("thread pool init\n");
int num[20], i;
for (i = 0; i < 20; ++i)
{
num[i] = i;
printf("add task %d\n", i);
// 向线程池中添加任务
// int threadpool_add(threadpool_t *pool, void *(*function)(void *arg), void *arg)
threadpool_add(thp, process, (void *)&num[i]);
}
sleep(5);
threadpool_destroy(thp);
return 0;
}
线程池的惊群效应
惊群效应就是多进程(多线程)同时阻塞在等待同一事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒所有等待的进程(或者线程),但最终只有一个线程会获取到事件的控制权,对该事件进行处理,而其他进程(线程)则会获取控制权失败,重新进入休眠状态,这种现象和资源的浪费就称之为惊群现象。
惊群现象会出现的问题
- 系统对用户进程/线程,做频发的进行上下文切换,无效的调度,这样系统性能会大打折扣
- 为了确保只有一个线程得到了资源,用户必须对资源操作进行加锁保护,这样加大了系统的开销
线程池中的惊群
一个基本的线程池框架是基于生产者和消费者模型的。生产者往队列里面添加任务,而消费者从队列中取任务并进行执行。一般来说,消费时间比较长,一般有许多个消费者。当许多个消费者同时在等待任务队列的时候,也就发生了“惊群效应”。
pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
但使用pthread_cond_signal不会有“惊群现象”产生,它最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发送信一号次。所以线程池并不会产生“惊群效应”。同时,这种方式使用多进程共享资源,等待管道或者其他资源等,提供cpu利用率。
怎么判断发生了惊群现象
我们根据strace的返回信息可以确定:
-
系统只会让一个进程真正的接受这个连接,而剩余的进程会获得一个EAGAIN信号。
-
通过返回结果和进程执行的系统调用判断。
惊群现象的解决方法
Linux内核的3.9版本带来了SO_REUSEPORT特性,该特性支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,允许多个套接字bind()以及listen()同一个TCP或UDP端口,并且在内核层面实现负载均衡。
未启用SO_REUSEPORT时,由一个socket将新接收的请求,交给各个工作者处理。
启用SO_REUSEPORT时,多个进程可以同时监听同一个IP:端口,然后由内核决定将新链接发送给哪个进程,显然会降低每个工人接收新链接时锁竞争。
【SO_REUSEPORT解决了什么问题】:
- 允许多个套接字bind()/listen()同一个tcp/udp端口。每一个线程拥有自己的服务器套接字,在服务器套接字上没有锁的竞争。
- 内核层面实现负载均衡。
- 安全层面,监听同一个端口的套接字只能位于同一个用户下面。
- 处理新建连接时,查找listener的时候,能够支持在监听相同IP和端口的多个sock之间均衡选择