最近開始寫小冊子,一篇篇來,寫完了再整理總結到一起。
Libuv是基於事件驅動的異步io庫,他本身是一個單進程單線程的。但是難免會有耗時的操作。如果在Libuv的主循環裏執行的話,就會阻塞後面的任務執行。所以Libuv裏維護了一個線程池。他負責處理Libuv中耗時的操作,比如文件io、dns、用戶自定義的耗時任務。文件io因爲存在跨平臺兼容的問題。無法很好地在事件驅動模塊實現異步io。下面分析一下線程池的實現。
我們先看線程池的初始化然後再看他的使用。
static void init_threads(void) {
unsigned int i;
const char* val;
uv_sem_t sem;
// 默認線程數4個,static uv_thread_t default_threads[4];
nthreads = ARRAY_SIZE(default_threads);
// 判斷用戶是否在環境變量中設置了線程數,是的話取用戶定義的
val = getenv("UV_THREADPOOL_SIZE");
if (val != NULL)
nthreads = atoi(val);
if (nthreads == 0)
nthreads = 1;
// #define MAX_THREADPOOL_SIZE 128最多128個線程
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);
QUEUE_INIT(&slow_io_pending_wq);
QUEUE_INIT(&run_slow_work_message);
// 初始化信號量變量,值爲0
if (uv_sem_init(&sem, 0))
abort();
// 創建多個線程,工作函數爲worker,sem爲worker入參
for (i = 0; i < nthreads; i++)
if (uv_thread_create(threads + i, worker, &sem))
abort();
//爲0則阻塞,非0則減一,這裏等待所有線程啓動成功再往下執行
for (i = 0; i < nthreads; i++)
uv_sem_wait(&sem);
uv_sem_destroy(&sem);
}
線程池的初始化主要是初始化一些數據結構,然後創建多個線程。接着在每個線程裏執行worker函數。worker是消費者,在分析消費者之前,我們先看一下生產者的邏輯。
static void init_once(void) {
init_threads();
}
// 給線程池提交一個任務
void uv__work_submit(uv_loop_t* loop,
struct uv__work* w,
enum uv__work_kind kind,
void (*work)(struct uv__work* w),
void (*done)(struct uv__work* w, int status)) {
// 保證已經初始化線程,並只執行一次,所以線程池是在提交第一個任務的時候才被初始化
uv_once(&once, init_once);
w->loop = loop;
w->work = work;
w->done = done;
post(&w->wq, kind);
}
這裏把業務相關的函數和任務完成後的回調函數封裝到uv__work結構體中。uv__work結構定義如下。
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];
};
然後調post往線程池的隊列中加入一個新的任務。Libuv把任務分爲三種類型,慢io(dns解析)、快io(文件操作)、cpu密集型等,kind就是說明任務的類型的。我們接着看post函數。
// 把任務插入隊列等待線程處理
static void post(QUEUE* q, enum uv__work_kind kind) {
// 加鎖訪問任務隊列,因爲這個隊列是線程池共享的
uv_mutex_lock(&mutex);
// 類型是慢IO
if (kind == UV__WORK_SLOW_IO) {
/*
插入慢IO對應的隊列,llibuv這個版本把任務分爲幾種類型,
對於慢io類型的任務,libuv是往任務隊列裏面插入一個特殊的節點
run_slow_work_message,然後用slow_io_pending_wq維護了一個慢io任務的隊列,
當處理到run_slow_work_message這個節點的時候,libuv會從slow_io_pending_wq
隊列裏逐個取出任務節點來執行。
*/
QUEUE_INSERT_TAIL(&slow_io_pending_wq, q);
/*
有慢IO任務的時候,需要給主隊列wq插入一個消息節點run_slow_work_message,
說明有慢IO任務,所以如果run_slow_work_message是空,說明還沒有插入主隊列。
需要進行q = &run_slow_work_message;賦值,然後把run_slow_work_message插入
主隊列。如果run_slow_work_message非空,說明已經插入線程池的任務隊列了。
解鎖然後直接返回。
*/
if (!QUEUE_EMPTY(&run_slow_work_message)) {
uv_mutex_unlock(&mutex);
return;
}
// 說明run_slow_work_message還沒有插入隊列,準備插入隊列
q = &run_slow_work_message;
}
// 把節點插入主隊列,可能是慢IO消息節點或者一般任務
QUEUE_INSERT_TAIL(&wq, q);
// 有空閒線程則喚醒他,如果大家都在忙,則等到他忙完後就會重新判斷是否還有新任務
if (idle_threads > 0)
uv_cond_signal(&cond);
uv_mutex_unlock(&mutex);
}
這就是libuv中線程池的生產者邏輯。架構如下。
除了上面提到的,libuv還提供了另外一種生產者。即uv_queue_work函數。他只針對cpu密集型的。從實現來看,他和第一種生產模式的區別是,通過uv_queue_work提交的任務,是對應一個request的。如果該request對應的任務沒有執行完,則事件循環不會退出。而通過uv__work_submit方式提交的任務就算沒有執行完,也不會影響事件循環的退出。下面我們看uv_queue_work的實現。
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__WORK_CPU,
uv__queue_work,
uv__queue_done);
return 0;
}
uv_queue_work函數其實也沒有太多的邏輯,他保存用戶的工作函數和回調到request中。然後提交任務,然後把uv__queue_work和uv__queue_done封裝到uv__work中,接着提交任務。所以當這個任務被執行的時候。他會執行工作函數uv__queue_work。
static void uv__queue_work(struct uv__work* w) {
// 通過結構體某字段拿到結構體地址
uv_work_t* req = container_of(w, uv_work_t, work_req);
req->work_cb(req);
}
我們看到uv__queue_work其實就是對用戶定義的任務函數進行了封裝。這時候我們可以猜到,uv__queue_done也只是對用戶回調的簡單封裝,即他會執行用戶的回調。至此,我們分析完了libuv中,線程池的兩種生產任務的方式。下面我們開始分析消費者。消費者由worker函數實現。
static void worker(void* arg) {
struct uv__work* w;
QUEUE* q;
int is_slow_work;
// 線程啓動成功
uv_sem_post((uv_sem_t*) arg);
arg = NULL;
// 加鎖互斥訪問任務隊列
uv_mutex_lock(&mutex);
for (;;) {
/*
1 隊列爲空,
2 隊列不爲空,但是隊列裏只有慢IO任務且正在執行的慢IO任務個數達到閾值
則空閒線程加一,防止慢IO佔用過多線程,導致其他快的任務無法得到執行
*/
while (QUEUE_EMPTY(&wq) ||
(QUEUE_HEAD(&wq) == &run_slow_work_message &&
QUEUE_NEXT(&run_slow_work_message) == &wq &&
slow_io_work_running >= slow_work_thread_threshold())) {
idle_threads += 1;
// 阻塞,等待喚醒
uv_cond_wait(&cond, &mutex);
// 被喚醒,開始幹活,空閒線程數減一
idle_threads -= 1;
}
// 取出頭結點,頭指點可能是退出消息、慢IO,一般請求
q = QUEUE_HEAD(&wq);
// 如果頭結點是退出消息,則結束線程
if (q == &exit_message) {
// 喚醒其他因爲沒有任務正阻塞等待任務的線程,告訴他們準備退出
uv_cond_signal(&cond);
uv_mutex_unlock(&mutex);
break;
}
// 移除節點
QUEUE_REMOVE(q);
// 重置前後指針
QUEUE_INIT(q);
is_slow_work = 0;
/*
如果當前節點等於慢IO節點,上面的while只判斷了是不是隻有慢io任務且達到
閾值,這裏是任務隊列裏肯定有非慢io任務,可能有慢io,如果有慢io並且正在 執行的個數達到閾值,則先不處理該慢io任務,繼續判斷是否還有非慢io任務可
執行。
*/
if (q == &run_slow_work_message) {
// 遇到閾值,重新入隊
if (slow_io_work_running >= slow_work_thread_threshold()) {
QUEUE_INSERT_TAIL(&wq, q);
continue;
}
// 沒有慢IO任務則繼續
if (QUEUE_EMPTY(&slow_io_pending_wq))
continue;
// 有慢io,開始處理慢IO任務
is_slow_work = 1;
// 正在處理慢IO任務的個數累加,用於其他線程判斷慢IO任務個數是否達到閾值
slow_io_work_running++;
// 摘下一個慢io任務
q = QUEUE_HEAD(&slow_io_pending_wq);
QUEUE_REMOVE(q);
QUEUE_INIT(q);
/*
取出一個任務後,如果還有慢IO任務則把慢IO標記節點重新入隊,
表示還有慢IO任務,因爲上面把該標記節點出隊了
*/
if (!QUEUE_EMPTY(&slow_io_pending_wq)) {
QUEUE_INSERT_TAIL(&wq, &run_slow_work_message);
// 有空閒線程則喚醒他,因爲還有任務處理
if (idle_threads > 0)
uv_cond_signal(&cond);
}
}
// 不需要操作隊列了,儘快釋放鎖
uv_mutex_unlock(&mutex);
// q是慢IO或者一般任務
w = QUEUE_DATA(q, struct uv__work, wq);
// 執行業務的任務函數,該函數一般會阻塞
w->work(w);
// 準備操作loop的任務完成隊列,加鎖
uv_mutex_lock(&w->loop->wq_mutex);
// 置空說明指向完了,不能被取消了,見cancel邏輯
w->work = NULL;
// 執行完任務,插入到loop的wq隊列,在uv__work_done的時候會執行該隊列的節點
QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
// 通知loop的wq_async節點
uv_async_send(&w->loop->wq_async);
uv_mutex_unlock(&w->loop->wq_mutex);
// 爲下一輪操作任務隊列加鎖
uv_mutex_lock(&mutex);
// 執行完慢IO任務,記錄正在執行的慢IO個數變量減1,上面加鎖保證了互斥訪問這個變量
if (is_slow_work) {
slow_io_work_running--;
}
}
}
我們看到消費者的邏輯似乎比較複雜,主要是把任務分爲三種。並且對於慢io類型的任務,還限制了線程數。其餘的邏輯和一般的線程池類型,就是互斥訪問任務隊列,然後取出節點執行,最後執行回調。不過libuv這裏不是直接回調用戶的函數。而是通過uv_async_send(&w->loop->wq_async)通知主進程有任務完成了。然後線程繼續執行任務。我們看一下這個函數的實現。
// 通知主線程有任務完成
int uv_async_send(uv_async_t* handle) {
/*
1 pending是0,則設置爲1,返回0,
2 pending是1則返回1,
所以同一個async如果多次調用該函數是會被合併的。只有pending等於
0的時候纔會執行uv__async_send
*/
if (cmpxchgi(&handle->pending, 0, 1) == 0)
uv__async_send(handle->loop);
return 0;
}
static void uv__async_send(uv_loop_t* loop) {
const void* buf;
ssize_t len;
int fd;
int r;
buf = "";
len = 1;
// 用於異步通信的管道的寫端
fd = loop->async_wfd;
#if defined(__linux__)
// fd等於1說明用的是eventfd而不是管道,管道纔有兩端
if (fd == -1) {
static const uint64_t val = 1;
buf = &val;
len = sizeof(val);
// 見uv__async_start
fd = loop->async_io_watcher.fd; /* eventfd */
}
#endif
// 通知讀端
do
r = write(fd, buf, len);
while (r == -1 && errno == EINTR);
// 省略部分代碼
}
uv__async_send通過網eventfd中寫入一些數據,觸發了對應io觀察者的事件。之前在分析async機制的時候講過。該io觀察者的回調是uv__work_done函數。那麼我們就看看這個函數的邏輯。
void uv__work_done(uv_async_t* handle) {
struct uv__work* w;
uv_loop_t* loop;
QUEUE* q;
QUEUE wq;
int err;
// 通過結構體字段獲得結構體首地址
loop = container_of(handle, uv_loop_t, wq_async);
// 準備處理隊列,加鎖
uv_mutex_lock(&loop->wq_mutex);
// 把loop->wq隊列的節點全部移到wp變量中,這樣一來可以儘快釋放鎖
QUEUE_MOVE(&loop->wq, &wq);
// 不需要使用了,解鎖
uv_mutex_unlock(&loop->wq_mutex);
// wq隊列的節點來源是在線程的worker裏插入
while (!QUEUE_EMPTY(&wq)) {
q = QUEUE_HEAD(&wq);
QUEUE_REMOVE(q);
w = container_of(q, struct uv__work, wq);
err = (w->work == uv__cancelled) ? UV_ECANCELED : 0;
// 執行回調
w->done(w, err);
}
}
逐個處理已完成的任務節點,執行回調。這就是整個消費者的邏輯。最後順帶提一下w->work == uv__cancelled。這個處理的用處是爲了支持取消一個任務。Libuv提供了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);
// 加鎖,爲了判斷w->wq是否爲空
uv_mutex_lock(&w->loop->wq_mutex);
/*
w在在任務隊列中並且任務函數work不爲空,則可取消,
在work函數中,如果執行完了任務,會把work置NULL,
所以一個任務可以取消的前提是他還沒執行完。或者說還沒執行過
*/
cancelled = !QUEUE_EMPTY(&w->wq) && w->work != NULL;
// 從任務隊列中刪除該節點
if (cancelled)
QUEUE_REMOVE(&w->wq);
uv_mutex_unlock(&w->loop->wq_mutex);
uv_mutex_unlock(&mutex);
// 不能取消
if (!cancelled)
return UV_EBUSY;
// 重置回調函數
w->work = uv__cancelled;
uv_mutex_lock(&loop->wq_mutex);
/*
插入loop的wq隊列,對於取消的動作,libuv認爲是任務執行完了。
所以插入已完成的隊列,不過他的回調是uv__cancelled函數,
而不是用戶設置的回調
*/
QUEUE_INSERT_TAIL(&loop->wq, &w->wq);
// 通知主線程有任務完成
uv_async_send(&loop->wq_async);
uv_mutex_unlock(&loop->wq_mutex);
return 0;
}
最後我們舉一個使用線程池的例子。這裏以文件操作爲例子,因爲nodejs中文件讀寫是以線程池實現的。這裏直接從uv_fs_open開始(因爲js層到c++層主要是一些封裝。最後會調到uv_fs_open)。直接看一下uv_fs_open的代碼。
// 下面代碼是宏展開後的效果
int uv_fs_open(
uv_loop_t* loop,
uv_fs_t* req,
const char* path,
int flags,
int mode,
uv_fs_cb cb
) {
// 初始化一些字段
UV_REQ_INIT(req, UV_FS);
req->fs_type = UV_FS_ ## subtype;
req->result = 0;
req->ptr = NULL;
req->loop = loop;
req->path = NULL;
req->new_path = NULL;
req->bufs = NULL;
req->cb = cb;
// 同步
if (cb == NULL) {
req->path = path;
} else {
req->path = uv__strdup(path);
}
req->flags = flags;
req->mode = mode;
if (cb != NULL) {
uv__req_register(loop, req);
/* 異步*/
uv__work_submit(
loop,
&req->work_req,
UV__WORK_FAST_IO,
uv__fs_work,
uv__fs_done
);
return 0;
}
else {
/* 同步 */
uv__fs_work(&req->work_req);
return req->result;
}
我們從上往下看,沒有太多的邏輯,函數的最後一個參數cb是nodejs的c++層設置的,c++層會再回調js層。然後open(大部分的文件操作)分爲同步和異步兩種模式(即fs.open和openSync)。同步直接導致nodejs阻塞,不涉及到線程池,這裏只看異步模式。我們看到異步模式下是調用uv__work_submit函數給線程池提交一個任務。設置的工作函數和回調函數分別是uv__fs_work,uv__fs_done。所以我們看一下這兩函數。uv__fs_work函數主要是調用操作系統提供的函數。比如open。他會引起線程的阻塞,等到執行完後,他會把返回結果保存到request結構體中。接着執行就是遵從線程池的處理流程。執行回調uv__fs_done。
static void uv__fs_done(struct uv__work* w, int status) {
uv_fs_t* req;
req = container_of(w, uv_fs_t, work_req);
uv__req_unregister(req->loop, req);
// 取消了
if (status == UV_ECANCELED) {
req->result = UV_ECANCELED;
}
// 執行用戶設置的回調,比如nodejs
req->cb(req);
}
沒有太多邏輯,直接執行回調,順便提一下,nodejs裏則是執行c++層函數AfterInteger(代碼在node_file.cc的Open函數)。
void AfterInteger(uv_fs_t* req) {
FSReqWrap* req_wrap = static_cast<FSReqWrap*>(req->data);
FSReqAfterScope after(req_wrap, req);
if (after.Proceed())
req_wrap->Resolve(Integer::New(req_wrap->env()->isolate(), req->result));
}
void FSReqWrap::Resolve(Local<Value> value) {
Local<Value> argv[2] {
Null(env()->isolate()),
value
};
MakeCallback(env()->oncomplete_string(), arraysize(argv), argv);
}
執行resolve,然後執行js層的oncomplete回調,即用戶執行open函數時傳入的函數。至此,線程池分析完成。