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,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

  1. ngx_thread_cond_create
    条件变量创建
  2. 线程创建
    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 主要函数

线程运行流程

  1. 线程运行函数 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,等待下一个插入

nginx开启多线程 nginx线程池_c语言


再来一个

nginx开启多线程 nginx线程池_线程池_02


就是这么个逻辑

接着走流程

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, 表明结束,就不用回调通知了

最后销毁条件变量和互斥锁。

到这就结束了!