nodejs事件循環階段之定時器

blank

nodejs事件循環階段之定時器

上一篇分析了prepare階段,check和idle階段是一樣的,所以就不分析了。今天分析定時器階段。 nodejs中setTimeout和setInterval就是使用libuv的定時器階段實現的。 libuv中,定時器是以最小堆實現的。即最快過期的節點是根節點。我看看看定時器的數據結構。

看一下定時器的使用。

int main() v_timer_t once; uv_timer_init(uv_default_loop(), &once); uv_timer_start(&once, once_cb, 10, 0); uv_run(uv_default_loop(), UV_RUN_DEFAULT); return 0; }

我們從uv_timer_init函數開始分析。

// 初始化uv_timer_t结构体int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle) { uv__handle_init(loop, (uv_handle_t*)handle, UV_TIMER); handle->timer_cb = NULL; handle->repeat = 0; return 0; }

init函數和其他階段的init函數一樣,初始化handle和私有的一些字段。接著我們看start函數。該函數是啟動一個定時器(省略部分代碼)。

// 启动一个计时器int uv_timer_start( uv_timer_t* handle, uv_timer_cb cb, uint64_t timeout, uint64_t repeat ) { uint64_t clamped_timeout; // 重新执行start的时候先把之前的停掉if (uv__is_active(handle)) uv_timer_stop(handle); // 超时时间,为绝对值clamped_timeout = handle->loop->time + timeout; // 初始化回调,超时时间,是否重复计时,赋予一个独立无二的id handle->timer_cb = cb; handle->timeout = clamped_timeout; handle->repeat = repeat; /* start_id is the second index to be compared in uv__timer_cmp() */ handle->start_id = handle->loop->timer_counter++; // 插入最小堆heap_insert(timer_heap(handle->loop), (struct heap_node*) &handle->heap_node, timer_less_than); // 激活该handle uv__handle_start(handle); return 0; }

start函數首先初始化handle裡的某些字段,包括超時回調,是否重複啟動定時器、超時的絕對時間等。接著把handle節點插入到最小堆中。最後給這個handle打上標記,激活這個handle。這時候的結構體如下。

blank

這時候到了事件循環的timer階段。

// 找出已经超时的节点,并且执行里面的回调void uv__run_timers(uv_loop_t* loop) { struct heap_node* heap_node; uv_timer_t* handle; for (;;) { heap_node = heap_min(timer_heap(loop)); if (heap_node == NULL) break; handle = container_of(heap_node, uv_timer_t, heap_node); // 如果当前节点的时间大于当前时间则返回,说明后面的节点也没有超时if (handle->timeout > loop->time) break; // 移除该计时器节点,重新插入最小堆,如果设置了repeat的话uv_timer_stop(handle); uv_timer_again(handle); // 执行超时回调handle->timer_cb(handle); } }

libuv在每次事件循環開始的時候都會緩存當前的時間,在整個一輪的事件循環中,使用的都是這個緩存的時間。緩存了當前最新的時間後,就執行uv__run_timers,該函數的邏輯很明了,就是遍歷最小堆,找出當前超時的節點。因為堆的性質是父節點肯定比孩子小。所以如果找到一個節點,他沒有超時,則後面的節點也不會超時。對於超時的節點就知道他的回調。執行完回調後,還有兩個關鍵的操作。第一就是stop,第二就是again。

// 停止一个计时器int uv_timer_stop(uv_timer_t* handle) { if (!uv__is_active(handle)) return 0; // 从最小堆中移除该计时器节点heap_remove(timer_heap(handle->loop), (struct heap_node*) &handle->heap_node, timer_less_than); // 清除激活状态和handle的active数减一uv__handle_stop(handle); return 0; }

stop的邏輯很簡單,其實就是把handle從二叉堆中刪除。並且取消激活狀態。那麼againt又是什麼呢? again是為了支持setInterval這種場景。

// 重新启动一个计时器,需要设置repeat标记int uv_timer_again(uv_timer_t* handle) { // 如果设置了repeat标记说明计时器是需要重复触发的if (handle->repeat) { // 先把旧的计时器节点从最小堆中移除,然后再重新开启一个计时器uv_timer_stop(handle); uv_timer_start(handle, handle->timer_cb, handle->repeat, handle->repeat); } return 0; }

如果handle設置了repeat標記,則該handle在超時後,每repeat的時間後,就會繼續執行超時回調。對於setInterval,就是超時時間是x,每x的時間後,執行回調。這就是nodejs裡定時器的底層原理。但nodejs不是每次調setTimeout的時候都往最小堆插入一個節點。 nodejs裡,只有一個關於uv_timer_s的handle。他在js層維護了一個數據結構,每次計算出最早到期的節點,然後修改handle的超時時間。具體原理在之前的一篇文章已經分析過。
timer階段和poll io階段也有一些聯繫,因為poll io可能會導致主線程阻塞,為了保證主線程可以盡快執行定時器的回調,poll io不能一直阻塞,所以這時候,阻塞的時長就是最快到期的定時器節點的時長。

What do you think?

Written by marketer

blank

Sigi framework introduction

blank

ali-react-table:高性能React 表格組件