libuv源碼剖析(四): 高效線程池Threadpool

Introduction

在網絡編程中, 始終都是基於Reactor模型的變種, 無論怎麼演化, 核心組件都包括: Reactor實例(事件註冊, 註銷, 通知); 多路複用器(由操作系統提供, 比如kqueue, select, epoll); 事件處理器(handler)以及事件源(linux中這就是描述符)這四個組件.

一般,會單獨啓動一個線程運行Reactor實例來實現真正的異步操作。但是,依賴操作系統提供的系統調用來實現異步是有侷限的,比如在Reactor模型中我們只能監聽到:網絡IO事件、signel(信號)、超時事件以及一些管道事件等,但這些事件也只是通知我們資源可讀或者可寫,真正的讀寫操作(read和write)還是同步的(也就是你必須等到read或者write返回,雖然linux提供了aio,但是其有諸多槽點),那麼libuv的全異步是如何做到的呢?你可能會很快想到,就是啓用單獨的線程來做同步的事情,這也是libuv的設計思路,借用官網的一張圖,說明一切:

這裏寫圖片描述

由上圖可以看到,libuv實現了一套自己的線程池來處理所有同步操作(從而模擬出異步的效果),下面就來看一下該線程池的具體實現吧!

線程池模型

幾乎所有的線程池都遵守着下面這個模型(任務隊列+線程池):

這裏寫圖片描述

在libuv中, 事件隊列藉助自身的高線隊列實現, 具體實現可參考我的另一篇博文: libuv源碼剖析(一): 高效隊列 Queue

接下來我們來看 threadpool 部分的實現.

首先, libuv對於 task 的定義:

struct uv__work {
  void (*work)(struct uv__work *w);
  void (*done)(struct uv__work *w, int status);
  struct uv_loop_s* loop;
  void* wq[2];
};

兩個回調函數指針(一個是實際任務, 一個是任務做完之後的回調), void *wq[2]work queue 中的節點, 通過這個節點組成一條鏈.
至於loop用來標明在哪個loop中.

再來看下 threadpool 初始化的過程:

#define MAX_THREADPOOL_SIZE 128

static uv_once_t once = UV_ONCE_INIT;
static uv_cond_t cond;
static uv_mutex_t mutex;
static unsigned int idle_threads;//當前空閒的線程數
static unsigned int nthreads;
static uv_thread_t* threads;
static uv_thread_t default_threads[4];
static QUEUE exit_message;
static QUEUE wq;//線程池全部會檢查這個queue,一旦發現有任務就執行,但是只能有一個線程搶佔到
static volatile int initialized;


static void init_once(void) {
  unsigned int i;
  const char* val;
  // 線程池中的線程數,默認值爲4
  nthreads = ARRAY_SIZE(default_threads);
  val = getenv("UV_THREADPOOL_SIZE");
  if (val != NULL)
    nthreads = atoi(val);
  if (nthreads == 0)
    nthreads = 1;
  if (nthreads > MAX_THREADPOOL_SIZE)
    nthreads = MAX_THREADPOOL_SIZE;

  threads = default_threads;
  if (nthreads > ARRAY_SIZE(default_threads)) {
    // 分配線程句柄
    threads = uv__malloc(nthreads * sizeof(threads[0]));
    if (threads == NULL) {
      nthreads = ARRAY_SIZE(default_threads);
      threads = default_threads;
    }
  }
  // 初始化條件變量
  if (uv_cond_init(&cond))
    abort();
  // 初始化互斥鎖
  if (uv_mutex_init(&mutex))
    abort();

  // 初始化任務隊列
  QUEUE_INIT(&wq);

  // 創建nthreads個線程
  for (i = 0; i < nthreads; i++)
    if (uv_thread_create(threads + i, worker, NULL))
      abort();

  initialized = 1;
}

上面的代碼中,一共創建了nthreads個線程,那麼每個線程的執行代碼是什麼呢?由線程創建代碼:uv_thread_create(threads + i, worker, NULL),可以看到,每一個線程都是執行worker函數,下面看看worker函數都在做什麼:

/* To avoid deadlock with uv_cancel() it's crucial that the worker
 * never holds the global mutex and the loop-local mutex at the same time.
 */
static void worker(void* arg) {
  struct uv__work* w;
  QUEUE* q;

  (void) arg;

  for (;;) {
    // 因爲是多線程訪問,因此需要加鎖同步
    uv_mutex_lock(&mutex);

    // 如果任務隊列是空的
    while (QUEUE_EMPTY(&wq)) {
      // 空閒線程數加1
      idle_threads += 1;
      // 等待條件變量
      uv_cond_wait(&cond, &mutex);
      // 被喚醒之後,說明有任務被post到隊列,因此空閒線程數需要減1
      idle_threads -= 1;
    }

    // 取出隊列的頭部節點(第一個task)
    q = QUEUE_HEAD(&wq);

    if (q == &exit_message)
      uv_cond_signal(&cond);
    else {
      // 從隊列中移除這個task
      QUEUE_REMOVE(q);
      QUEUE_INIT(q);  /* Signal uv_cancel() that the work req is
                             executing. */
    }

    uv_mutex_unlock(&mutex);

    if (q == &exit_message)
      break;

    // 取出uv__work首地址
    w = QUEUE_DATA(q, struct uv__work, wq);
    // 調用task的work,執行任務
    w->work(w);

    uv_mutex_lock(&w->loop->wq_mutex);
    w->work = NULL;  /* Signal uv_cancel() that the work req is done
                        executing. */
    QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
    uv_async_send(&w->loop->wq_async);
    uv_mutex_unlock(&w->loop->wq_mutex);
  }
}

可以看到,多個線程都會在worker方法中等待在conn條件變量上,一旦有任務加入隊列,線程就會被喚醒,然後只有一個線程會得到任務的執行權,其他的線程只能繼續等待。
那麼如何向隊列提交一個task呢?看以下代碼:

void uv__work_submit(uv_loop_t* loop,
                 struct uv__work* w,
                 void (*work)(struct uv__work* w),
                 void (*done)(struct uv__work* w, int status)) {
  uv_once(&once, init_once);
  // 構造一個task
  w->loop = loop;
  w->work = work;
  w->done = done;
  // 將其插入任務隊列
  post(&w->wq);
}

接着看post做了什麼:

static void post(QUEUE* q) {
  // 同步隊列操作
  uv_mutex_lock(&mutex);
  // 將task插入隊列尾部
  QUEUE_INSERT_TAIL(&wq, q);
  // 如果當前有空閒線程,就向條件變量發送信號
  if (idle_threads > 0)
    uv_cond_signal(&cond);
  uv_mutex_unlock(&mutex);
}

有提交任務,就肯定會有取消一個任務的操作,是的,他就是uv__work_cancel,代碼如下:

static int uv__work_cancel(uv_loop_t* loop, uv_req_t* req, struct uv__work* w) {
  int cancelled;

  uv_mutex_lock(&mutex);
  uv_mutex_lock(&w->loop->wq_mutex);

  // 只有當前隊列不爲空並且要取消的uv__work有效時纔會繼續執行
  cancelled = !QUEUE_EMPTY(&w->wq) && w->work != NULL;
  if (cancelled)
    QUEUE_REMOVE(&w->wq);// 從隊列中移除task

  uv_mutex_unlock(&w->loop->wq_mutex);
  uv_mutex_unlock(&mutex);

  if (!cancelled)
    return UV_EBUSY;

  // 更新這個task的狀態
  w->work = uv__cancelled;
  uv_mutex_lock(&loop->wq_mutex);
  QUEUE_INSERT_TAIL(&loop->wq, &w->wq);
  uv_async_send(&loop->wq_async);
  uv_mutex_unlock(&loop->wq_mutex);

  return 0;
}

至此,一個線程池的組成以及實現原理都說完了,可以看到,libuv幾乎是用了最少的代碼完成了高效的線程池,這對於我們平時寫代碼時具有很好的借鑑意義,文中涉及到uv_req_t以及uv_loop_t等結構我都直接跳過,因爲這牽扯到libuv的其他組件,我將在以後的源碼剖析中逐步闡述,謝謝你能看到這裏。

舉個例子

接下來,我們再通過一個最簡單的栗子看libuv是如何使用這個線程池的.我們來看 uvbook/queue-work/main.c 中的示例代碼:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <uv.h>
#define FIB_UNTIL 5
uv_loop_t *loop;

long fib_(long t) {
    if (t == 0 || t == 1)
        return 1;
    else
        return fib_(t-1) + fib_(t-2);
}

void fib(uv_work_t *req) {
    int n = *(int *) req->data;
    if (random() % 2)
        sleep(1);
    else
        sleep(3);
    long fib = fib_(n);
    fprintf(stderr, "%dth fibonacci is %lu\n", n, fib);
}

void after_fib(uv_work_t *req, int status) {
    fprintf(stderr, "Done calculating %dth fibonacci\n", *(int *) req->data);
}

int main() {
    loop = uv_default_loop();

    int data[FIB_UNTIL];
    uv_work_t req[FIB_UNTIL];
    int i;
    for (i = 0; i < FIB_UNTIL; i++) {
        data[i] = i;
        req[i].data = (void *) &data[i];
        uv_queue_work(loop, &req[i], fib, after_fib);
    }

    return uv_run(loop, UV_RUN_DEFAULT);
}

上面的代碼的輸出如下:

0th fibonacci is 1
2th fibonacci is 2
Done calculating 0th fibonacci
Done calculating 2th fibonacci
3th fibonacci is 3
Done calculating 3th fibonacci
4th fibonacci is 5
Done calculating 4th fibonacci
1th fibonacci is 1
Done calculating 1th fibonacci

其中, uv_queue_work是一個很方便的函數允許應用在一個隔離的線程運行,並且結束之後調用 callback 函數, 我們來看下它的實現:

int uv_queue_work(uv_loop_t* loop,
                  uv_work_t* req,
                  uv_work_cb work_cb,
                  uv_after_work_cb after_work_cb) {
  if (work_cb == NULL)
    return UV_EINVAL;

  uv__req_init(loop, req, UV_WORK);
  req->loop = loop;
  req->work_cb = work_cb;
  req->after_work_cb = after_work_cb;
  uv__work_submit(loop, &req->work_req, uv__queue_work, uv__queue_done);
  return 0;
}

可以看出,就是通過這個函數調用了 uv__work_submit 來將任務提交到任務隊列中. 在使用 gdb 打斷點調試的時候, 能發現在第一次執行到 uv_queue_work 的時候, 就會立馬創建出 4 個線程, 並且立刻執行 work_cb 任務. 而 after_work_cb 則需要到 uv_run 的時候纔會執行

非常感謝大牛的博客, 參考鏈接:
https://my.oschina.net/fileoptions/blog/1036609

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章