nginx 线程池详解
基础知识:
需要了解队列、条件变量相关知识。
nginx异步思想,线程池设计同样如此。
A线程准备任务-----post任务-------线程池唤醒一个线程处理--------通过回调通知A线程处理完毕。整体就是这个样子的
以下是更详细的流程,有点长,捡着你想看的看。
线程池初始化
代码如下:
static ngx_int_t
ngx_thread_pool_init(ngx_thread_pool_t *tp, ngx_log_t *log, ngx_pool_t *pool)
{
int err;
pthread_t tid;
ngx_uint_t n;
pthread_attr_t attr;
if (ngx_notify == NULL) {
ngx_log_error(NGX_LOG_ALERT, log, 0,
"the configured event method cannot be used with thread pools");
return NGX_ERROR;
}
ngx_thread_pool_queue_init(&tp->queue);
if (ngx_thread_mutex_create(&tp->mtx, log) != NGX_OK) {
return NGX_ERROR;
}
if (ngx_thread_cond_create(&tp->cond, log) != NGX_OK) {
(void) ngx_thread_mutex_destroy(&tp->mtx, log);
return NGX_ERROR;
}
tp->log = log;
err = pthread_attr_init(&attr);
if (err) {
ngx_log_error(NGX_LOG_ALERT, log, err,
"pthread_attr_init() failed");
return NGX_ERROR;
}
err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (err) {
ngx_log_error(NGX_LOG_ALERT, log, err,
"pthread_attr_setdetachstate() failed");
return NGX_ERROR;
}
#if 0
err = pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN);
if (err) {
ngx_log_error(NGX_LOG_ALERT, log, err,
"pthread_attr_setstacksize() failed");
return NGX_ERROR;
}
#endif
for (n = 0; n < tp->threads; n++) {
err = pthread_create(&tid, &attr, ngx_thread_pool_cycle, tp);
if (err) {
ngx_log_error(NGX_LOG_ALERT, log, err,
"pthread_create() failed");
return NGX_ERROR;
}
}
(void) pthread_attr_destroy(&attr);
return NGX_OK;
}
1.ngx_thread_pool_queue_init
函数是初始化线程池的任务队列
(q)->first = NULL; \
(q)->last = &(q)->first
first指针赋值NULL,**last执行first的地址,一个队列插入技巧,在插入队列时介绍
2.ngx_thread_mutex_create
代码就不贴了
创建线程互斥锁,条件变量使用,锁请求队列用的
在此函数中pthread_mutexattr_settype设置锁类型:nginx使用PTHREAD_MUTEX_ERRORCHECK_NP
PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
- ngx_thread_cond_create
条件变量创建 - 线程创建
pthread_attr_setdetachstate 设置线程属性
for (n = 0; n < tp->threads; n++) {
err = pthread_create(&tid, &attr, ngx_thread_pool_cycle, tp);
if (err) {
ngx_log_error(NGX_LOG_ALERT, log, err,
"pthread_create() failed");
return NGX_ERROR;
}
}
创建线程ngx_thread_pool_cycle 主要函数
线程运行流程
- 线程运行函数 ngx_thread_pool_cycle
贴一下主要代码
static void *
ngx_thread_pool_cycle(void *data)
{
ngx_thread_pool_t *tp = data;
int err;
sigset_t set;
ngx_thread_task_t *task;
for ( ;; ) {
if (ngx_thread_mutex_lock(&tp->mtx, tp->log) != NGX_OK) {
return NULL;
}
/* the number may become negative */
tp->waiting--;
while (tp->queue.first == NULL) {
if (ngx_thread_cond_wait(&tp->cond, &tp->mtx, tp->log)
!= NGX_OK)
{
(void) ngx_thread_mutex_unlock(&tp->mtx, tp->log);
return NULL;
}
}
task = tp->queue.first;
tp->queue.first = task->next;
if (tp->queue.first == NULL) {
tp->queue.last = &tp->queue.first;
}
if (ngx_thread_mutex_unlock(&tp->mtx, tp->log) != NGX_OK) {
return NULL;
}
#if 0
ngx_time_update();
#endif
ngx_log_debug2(NGX_LOG_DEBUG_CORE, tp->log, 0,
"run task #%ui in thread pool \"%V\"",
task->id, &tp->name);
task->handler(task->ctx, tp->log);
ngx_log_debug2(NGX_LOG_DEBUG_CORE, tp->log, 0,
"complete task #%ui in thread pool \"%V\"",
task->id, &tp->name);
task->next = NULL;
ngx_spinlock(&ngx_thread_pool_done_lock, 1, 2048);
*ngx_thread_pool_done.last = task;
ngx_thread_pool_done.last = &task->next;
ngx_memory_barrier();
ngx_unlock(&ngx_thread_pool_done_lock);
(void) ngx_notify(ngx_thread_pool_handler);
}
}
这个函数是一个死循环,等待任务触发,处理后,线程休眠,下面详细说一下流程。
首先是一个for循环…
if (ngx_thread_mutex_lock(&tp->mtx, tp->log) != NGX_OK) {
return NULL;
}
在判断任务队列前,先上锁,霸占着再说
tp->waiting--;
while (tp->queue.first == NULL) {
if (ngx_thread_cond_wait(&tp->cond, &tp->mtx, tp->log)
!= NGX_OK)
{
(void) ngx_thread_mutex_unlock(&tp->mtx, tp->log);
return NULL;
}
}
waiting这个变量记录未处理的任务的个数
下面这个while循环是关键
判断tp-queue.first == NULL, 表示没有任务,ngx_thread_cond_wait 是线程休眠,等待其他线程唤醒
ngx_thread_cond_wait 第二个参数是mtx,这个任务互斥锁,在cond_wait是一个原子操作,先解锁,然后使进程休眠,等待唤醒,唤醒后再使用mtx加锁。这也是条件变量的精髓之一,自我感觉哈。
如果是非原子操作,伪代码如下:
ngx_thread_mutex_unlock();
cond_wait()
当线程执行完ngx_thread_mutex_unlock后,cpu切换线程,添加任务,执行ngx_thread_cond_signal唤醒线程执行任务,就会造成此任务丢失,所以ngx_thread_cond_wait必须是原子操作。
回到流程,
ngx_thread_cond_wait 休眠线程,具体原理不太明白,它的唤醒条件:
(1)其他线程执行ngx_thread_cond_signal 或者 pthread_cond_broadcast() ,nginx中使用ngx_thread_cond_signal 表示唤醒一个等待进程。
(2)一些系统信号,可能会唤醒。
由于第二种情况干扰,休眠被唤醒后,必须再次判断条件,也就是任务队列中是否有数据,这也就是while循环的原因。
唤醒后,处理流程:
task = tp->queue.first;
tp->queue.first = task->next;
if (tp->queue.first == NULL) {
tp->queue.last = &tp->queue.first;
}
if (ngx_thread_mutex_unlock(&tp->mtx, tp->log) != NGX_OK) {
return NULL;
}
将队列的第一个task取出来,然后将first指针指向下一个。
判断first == NULL,取出后,如果队列为空,将last指向first的地址,和初始化是一样,队列从last插入流程,等会添加任务时介绍。
一次只执行一个task,取出后,将任务互斥锁解锁。添加的时候也是只能一次添加一个,是对应处理。
task->handler(task->ctx, tp->log);
task->next = NULL;
执行task任务
将task->next赋值NULL,这个处理后的任务,将要插入到完成队列,这个next在此必须赋值为NULL。
ngx_spinlock(&ngx_thread_pool_done_lock, 1, 2048);
*ngx_thread_pool_done.last = task;
ngx_thread_pool_done.last = &task->next;
ngx_memory_barrier();
ngx_unlock(&ngx_thread_pool_done_lock);
(void) ngx_notify(ngx_thread_pool_handler);
ngx_spinlock自旋锁,不用睡眠等待的方式,使用cpu周期等待条件成立,反应快,但消耗cpu,在此只添加队列,取队列,很快,并且在取队列的时候,是一次取一个链条,不是一个一个取,等一会可以看到。
ngx_thread_pool_done是一个全局变量,nginx是可以配置多个线程池,每个线程池,有独自的任务链表,任务结束后,是插入到一个统一的完成链表中,等待通知进程,回调告知任务执行完毕。这也可以看出,任务结束的回调会有一定延时,如果在通知回调中时间太长,可能会影响后续处理。
ngx_memory_barrier(); 不太懂,望高人指教。
ngx_notify 通知进程,ngx_thread_pool_handler是回调处理函数
ngx_notify 这个函数处理,在nginx中好像只给线程池使用,可以看epoll模块,使用eventfd技术,在这就不说了,主要也不太明白,哈哈哈。
线程任务结束通知
实际就是ngx_thread_pool_handler 的处理。
代码如下:
static void
ngx_thread_pool_handler(ngx_event_t *ev)
{
ngx_event_t *event;
ngx_thread_task_t *task;
ngx_log_debug0(NGX_LOG_DEBUG_CORE, ev->log, 0, "thread pool handler");
ngx_spinlock(&ngx_thread_pool_done_lock, 1, 2048);
task = ngx_thread_pool_done.first;
ngx_thread_pool_done.first = NULL;
ngx_thread_pool_done.last = &ngx_thread_pool_done.first;
ngx_memory_barrier();
ngx_unlock(&ngx_thread_pool_done_lock);
while (task) {
ngx_log_debug1(NGX_LOG_DEBUG_CORE, ev->log, 0,
"run completion handler for task #%ui", task->id);
event = &task->event;
task = task->next;
event->complete = 1;
event->active = 0;
event->handler(event);
}
}
task = ngx_thread_pool_done.first;
ngx_thread_pool_done.first = NULL;
ngx_thread_pool_done.last = &ngx_thread_pool_done.first;
task是整个处理完成的链条,直接将frist赋值NULL,last指针初始化。避免一个一个取,增加自旋锁的时间,将时间成本,减少到底。
以下是while循环调用task的通知回调函数,完毕
任务推送流程
其他线程,编辑task,通过ngx_thread_task_post 函数,推送执行
代码如下:
ngx_int_t
ngx_thread_task_post(ngx_thread_pool_t *tp, ngx_thread_task_t *task)
{
if (task->event.active) {
ngx_log_error(NGX_LOG_ALERT, tp->log, 0,
"task #%ui already active", task->id);
return NGX_ERROR;
}
if (ngx_thread_mutex_lock(&tp->mtx, tp->log) != NGX_OK) {
return NGX_ERROR;
}
if (tp->waiting >= tp->max_queue) {
(void) ngx_thread_mutex_unlock(&tp->mtx, tp->log);
ngx_log_error(NGX_LOG_ERR, tp->log, 0,
"thread pool \"%V\" queue overflow: %i tasks waiting",
&tp->name, tp->waiting);
return NGX_ERROR;
}
task->event.active = 1;
task->id = ngx_thread_pool_task_id++;
task->next = NULL;
if (ngx_thread_cond_signal(&tp->cond, tp->log) != NGX_OK) {
(void) ngx_thread_mutex_unlock(&tp->mtx, tp->log);
return NGX_ERROR;
}
*tp->queue.last = task;
tp->queue.last = &task->next;
tp->waiting++;
(void) ngx_thread_mutex_unlock(&tp->mtx, tp->log);
ngx_log_debug2(NGX_LOG_DEBUG_CORE, tp->log, 0,
"task #%ui added to thread pool \"%V\"",
task->id, &tp->name);
return NGX_OK;
}
想要操作任务队列,必须要拿到互斥锁
判断tp->waiting >= tp->max_queue ,max_queue 是一个配置项参数
ngx_thread_cond_signal 唤醒一个处理线程(随机的),然后唤醒线程会被互斥锁堵塞,我这还没开呢
*tp->queue.last = task;
tp->queue.last = &task->next;
插入队列
流程如下,自己记录一下,以后复习看
加入此时是第一个task
初始化时tp->queue.last = &first;
*tp->queue.last = task; ===> first = task;
然后tp->queue.last = &task->next; 将next的地址赋值到last,等待下一个插入
再来一个
就是这么个逻辑
接着走流程
ngx_thread_mutex_unlock()解锁,此时被唤醒的线程,上锁成功,就可以取到task了。
线程池的销毁
在进程退出时,会销毁线程
static void
ngx_thread_pool_destroy(ngx_thread_pool_t *tp)
{
ngx_uint_t n;
ngx_thread_task_t task;
volatile ngx_uint_t lock;
ngx_memzero(&task, sizeof(ngx_thread_task_t));
task.handler = ngx_thread_pool_exit_handler;
task.ctx = (void *) &lock;
for (n = 0; n < tp->threads; n++) {
lock = 1;
if (ngx_thread_task_post(tp, &task) != NGX_OK) {
return;
}
while (lock) {
ngx_sched_yield();
}
task.event.active = 0;
}
(void) ngx_thread_cond_destroy(&tp->cond, tp->log);
(void) ngx_thread_mutex_destroy(&tp->mtx, tp->log);
}
创建一个task,线程调用的最简流程了
一个一个线程退出
for循环线程个数
lock = 1;
在ngx_thread_pool_exit_handler 中赋值为 0, 表明结束,就不用回调通知了
最后销毁条件变量和互斥锁。
到这就结束了!