我們發現事件驅動的軟件都得配一個線程池。libuv和nginx都是。因爲事件驅動的軟件是單線程。但是有些事情總會引起線程阻塞。所以這個事情就不能放到主線程裏做。這就是爲什麼事件驅動都要配一個線程池。把任務交給線程池中的線程。主線程繼續執行。任務完成後通知主線程或者執行回調就行。
我們先看一下nginx線程池的架構。然後開始分析。
線程池模塊在nginx裏屬於核心模塊。在nginx初始化的時候。會初始化一個保存線程池配置的結構體(見圖)。nginx默認開啓四個線程池。
static void *
ngx_thread_pool_create_conf(ngx_cycle_t *cycle)
{
ngx_thread_pool_conf_t *tcf;
tcf = ngx_pcalloc(cycle->pool, sizeof(ngx_thread_pool_conf_t));
if (tcf == NULL) {
return NULL;
}
if (ngx_array_init(&tcf->pools, cycle->pool, 4,
sizeof(ngx_thread_pool_t *))
!= NGX_OK)
{
return NULL;
}
return tcf;
}
上面的函數就是構造出文章開頭的那個圖的結構。創建了保存配置的結構,nginx開始解析指令。在分析解析指令前,我們先看一下幾個工具函數。
// 根據名字查找池子
ngx_thread_pool_t *
ngx_thread_pool_get(ngx_cycle_t *cycle, ngx_str_t *name)
{
ngx_uint_t i;
ngx_thread_pool_t **tpp;
ngx_thread_pool_conf_t *tcf;
tcf = (ngx_thread_pool_conf_t *) ngx_get_conf(cycle->conf_ctx,
ngx_thread_pool_module);
tpp = tcf->pools.elts;
for (i = 0; i < tcf->pools.nelts; i++) {
if (tpp[i]->name.len == name->len
&& ngx_strncmp(tpp[i]->name.data, name->data, name->len) == 0)
{
return tpp[i];
}
}
return NULL;
}
nginx每個線程池都有一個名字,這個函數就是從圖裏面的數組中找到名字對應的線程池。
ngx_thread_pool_t *
ngx_thread_pool_add(ngx_conf_t *cf, ngx_str_t *name)
{
ngx_thread_pool_t *tp, **tpp;
ngx_thread_pool_conf_t *tcf;
// 沒有名字則取默認值
if (name == NULL) {
name = &ngx_thread_pool_default;
}
// 已存在直接返回
tp = ngx_thread_pool_get(cf->cycle, name);
if (tp) {
return tp;
}
// 分配一個新的池子
tp = ngx_pcalloc(cf->pool, sizeof(ngx_thread_pool_t));
if (tp == NULL) {
return NULL;
}
tp->name = *name;
tp->file = cf->conf_file->file.name.data;
tp->line = cf->conf_file->line;
// 拿到一開始時創建的,用於保存配置的結構體
tcf = (ngx_thread_pool_conf_t *) ngx_get_conf(cf->cycle->conf_ctx,
ngx_thread_pool_module);
// push進數組,數組會自動擴容
tpp = ngx_array_push(&tcf->pools);
if (tpp == NULL) {
return NULL;
}
*tpp = tp;
return tp;
}
上面的函數就是往數組中追加一個元素(表示線程池的結構體)。如果已經存在則報錯。
我們看一下,nginx如何解析指令的。配置線程池的指令是
thread_pool name threads=number [max_queue=number]
解析到這個指令的時候,nginx會執行ngx_thread_pool。
static char *
ngx_thread_pool(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_str_t *value;
ngx_uint_t i;
ngx_thread_pool_t *tp;
// thread_pool指令後的參數
value = cf->args->elts;
// 根據名字(沒有則取默認名字)新建一個結構體
tp = ngx_thread_pool_add(cf, &value[1]);
// threads有值說明之前已經配置過這個名字
if (tp->threads) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"duplicate thread pool \"%V\"", &tp->name);
return NGX_CONF_ERROR;
}
tp->max_queue = 65536;
// 解析剩下的參數
for (i = 2; i < cf->args->nelts; i++) {
if (ngx_strncmp(value[i].data, "threads=", 8) == 0) {
// 設置線程數
tp->threads = ngx_atoi(value[i].data + 8, value[i].len - 8);
continue;
}
if (ngx_strncmp(value[i].data, "max_queue=", 10) == 0) {
// 設置任務個數上限
tp->max_queue = ngx_atoi(value[i].data + 10, value[i].len - 10);
continue;
}
}
// 等於0說明指令裏沒有配置threads參數,報錯max_queue可以不配
if (tp->threads == 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"%V\" must have \"threads\" parameter",
&cmd->name);
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}
上面的代碼主要構造文章開始那個圖中的結構。根據nginx的流程
1 創建保存配置的結構
2 解析配置
3 校驗和補償處理配置
解析完配置後,nginx接着校驗和補償處理。
// 處理完用戶的配置後,可能需要做補償處理
static char *
ngx_thread_pool_init_conf(ngx_cycle_t *cycle, void *conf)
{
ngx_thread_pool_conf_t *tcf = conf;
ngx_uint_t i;
ngx_thread_pool_t **tpp;
tpp = tcf->pools.elts;
// 圖中那個數組
for (i = 0; i < tcf->pools.nelts; i++) {
// 用戶已經配置了線程數
if (tpp[i]->threads) {
continue;
}
// 沒有配置線程數,但是取了默認名字,則其他信息也設置爲默認值
if (
tpp[i]->name.len == ngx_thread_pool_default.len
&&
ngx_strncmp(
tpp[i]->name.data,
ngx_thread_pool_default.data,
ngx_thread_pool_default.len)
== 0
)
{
tpp[i]->threads = 32;
tpp[i]->max_queue = 65536;
continue;
}
// 配置了名字但是沒有配置線程數,報錯
ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,
"unknown thread pool \"%V\" in %s:%ui",
&tpp[i]->name, tpp[i]->file, tpp[i]->line);
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}
到此,關於線程池的數據結構已經處理完畢。接下就是創建線程和初始化線程池的數據了。在每個worker初始化的時候,會根據線程池的配置,創建對應的線程。
static ngx_int_t
ngx_thread_pool_init_worker(ngx_cycle_t *cycle)
{
ngx_uint_t i;
ngx_thread_pool_t **tpp;
ngx_thread_pool_conf_t *tcf;
// 線程池只用於worker進程
if (ngx_process != NGX_PROCESS_WORKER
&& ngx_process != NGX_PROCESS_SINGLE)
{
return NGX_OK;
}
tcf = (ngx_thread_pool_conf_t *) ngx_get_conf(cycle->conf_ctx,
ngx_thread_pool_module);
if (tcf == NULL) {
return NGX_OK;
}
// 初始化隊列(已完成的任務)
ngx_thread_pool_queue_init(&ngx_thread_pool_done);
// 線程池結構體數組
tpp = tcf->pools.elts;
// 每個worker啓動一個或多個線程池
for (i = 0; i < tcf->pools.nelts; i++) {
if (ngx_thread_pool_init(tpp[i], cycle->log, cycle->pool) != NGX_OK) {
return NGX_ERROR;
}
}
return NGX_OK;
}
上面的代碼遍歷線程池結構體數組。針對每一個線程池結構體創建多個線程。
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;
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);
// 設置狀態爲分離,線程退出時資源馬上被回收,不需要等待父線程回收
err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 創建n個線程,工作函數是ngx_thread_pool_cycle,入參是tp
for (n = 0; n < tp->threads; n++) {
err = pthread_create(&tid, &attr, ngx_thread_pool_cycle, tp);
}
// 用完銷燬
(void) pthread_attr_destroy(&attr);
return NGX_OK;
}
這時候,多個線程就被創建了。然後每個線程執行自己的工作函數。
// 處理任務
static void *
ngx_thread_pool_cycle(void *data)
{
ngx_thread_pool_t *tp = data;
int err;
sigset_t set;
ngx_thread_task_t *task;
// 全置1
sigfillset(&set);
// 下面幾個信號清零
sigdelset(&set, SIGILL);
sigdelset(&set, SIGFPE);
sigdelset(&set, SIGSEGV);
sigdelset(&set, SIGBUS);
// 屏蔽除了上面幾個之外的信號
err = pthread_sigmask(SIG_BLOCK, &set, NULL);
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;
}
}
// 摘下第一個任務
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->handler(task->ctx, tp->log);
task->next = NULL;
ngx_spinlock(&ngx_thread_pool_done_lock, 1, 2048);
// 執行完插入done隊列尾部,done隊列是所有線程池公用的,任務隊列是每個線程池私有的
*ngx_thread_pool_done.last = task;
// 指向最後一個節點的next域的地址
ngx_thread_pool_done.last = &task->next;
ngx_memory_barrier();
ngx_unlock(&ngx_thread_pool_done_lock);
// 有任務完成,發通知
(void) ngx_notify(ngx_thread_pool_handler);
}
}
線程池維護了一個任務隊列,池中的線程互斥訪問隊列,從中摘下任務執行。任務執行完後把已完成的任務放到完成隊列中(所有線程池共享)。並且通知負責處理完成任務節點的函數。
static void
ngx_thread_pool_handler(ngx_event_t *ev)
{
ngx_event_t *event;
ngx_thread_task_t *task;
// 加鎖訪問隊列
ngx_spinlock(&ngx_thread_pool_done_lock, 1, 2048);
// 指向整個done隊列的節點,先保存下來,而不是直接遍歷first指針,否則回調裏一直加任務導致死循環
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) {
event = &task->event;
// 指向下一個節點
task = task->next;
// 設置完成標記
event->complete = 1;
event->active = 0;
event->handler(event);
}
}
這就是nginx線程池的原理。和大部分的線程池實現類似,代碼看起來很多,但是邏輯還是比較清晰的。