恭喜你,完成了上一节的学习,下面让咱们一起来分析一下定时器是如何实现的吧!
helloworld
- 首先,让咱们来一个最简单的 Libuv 程序
#include <stdio.h> #include <stdlib.h> #include <uv.h> int main() { uv_loop_t *loop = malloc(sizeof(uv_loop_t)); // 初始化给定的 uv_loop_t 结构体 uv_loop_init(loop); printf("Now quitting.\n"); // 运行事件循环 uv_run(loop, UV_RUN_DEFAULT); // 释放所有的内部循环资源 uv_loop_close(loop); free(loop); return 0; }
- 可以看到,一个最简单的程序是由初始化,运行,停止运行这三部分组成的。下面咱们来看看源码中是如何实现的呢?
源码分析
uv_loop_init
- 可以想象到,在 uv_loop_init 就是对 uv_loop_t 结构体执行各种初始化操作
- 因为 uv_loop_init 和 uv_loop_t 结构体代码有点长,现在给大家说了,有可能会把大家思维搞乱。而且 uv_loop_t 结构体中的内容在咱们之后的学习中会一直用到,所以这一部分内容咱们暂时就不进行分析了。
uv_run
- 咱们来看一下 uv_run 中的部分源码
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; r = uv__loop_alive(loop); if (!r) uv__update_time(loop); while (r != 0 && loop->stop_flag == 0) { // 更新时间并开始倒计时 loop->time uv__update_time(loop); uv__run_timers(loop); // 处理挂起的handle ran_pending = uv__run_pending(loop); // 运行idle handle uv__run_idle(loop); // 运行prepare handle uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv__backend_timeout(loop); // 计算要阻塞的时间,开始阻塞 uv__io_poll(loop, timeout); uv__metrics_update_idle_time(loop); // 程序执行到这里表示被唤醒了,被唤醒的原因可能是I/O可读可写、或者超时了,检查handle是否可以操作 uv__run_check(loop); // 看看是否有close的handle uv__run_closing_handles(loop); // 单次模式 if (mode == UV_RUN_ONCE) { uv__update_time(loop); uv__run_timers(loop); } // handle保活处理 r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } if (loop->stop_flag != 0) loop->stop_flag = 0; return r; }
- 是不是感觉很复杂呢?没关系,
uv_run
中的内容不需要大家一次就看懂,咱会分为好多个部分来进行讲解。主要是需要大家了解这个函数中通过while
循环来执行一些事件。这一讲,咱们来说一说while
循环中的第一个部分 —— 定时器。
定时器
- 在 Libuv 中,定时器是以最小堆来实现的,即最快过期的节点是根节点。
数据结构
struct uv_timer_s {
// 句柄[handle]相关参数
// uv_handle_t
void* data;
uv_loop_t* loop;
uv_handle_type type;
uv_close_cb close_cb;
void* handle_queue[2];
union {
int fd;
void* reserved[4];
} u;
uv_handle_t* next_closing;
unsigned int flags;
// 定时器相关参数
uv_timer_cb timer_cb;
void* heap_node[3];
uint64_t timeout;
uint64_t repeat;
uint64_t start_id;
}
相关操作
uv_timer_init
- 源码
// 初始化uv_timer_t结构体 int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle) { // 初始化 handle uv__handle_init(loop, (uv_handle_t*)handle, UV_TIMER); // 初始化一些私有字段 handle->timer_cb = NULL; handle->timeout = 0; handle->repeat = 0; return 0; }
uv_timer_start
- 启动一个计时器
- 源码
// 启动一个计时器
int uv_timer_start(uv_timer_t* handle,
uv_timer_cb cb,
uint64_t timeout,
uint64_t repeat) {
uint64_t clamped_timeout;
// 如果这个计时器句柄是关闭的或者回调函数为 NULL
if (uv__is_closing(handle) || cb == NULL)
return UV_EINVAL;
// 重新执行start的时候先把之前的停掉
if (uv__is_active(handle))
uv_timer_stop(handle);
// 超时时间,为绝对值
// handle->loop->time 是在 uv_run 中每一次 while 循环开始的时间
clamped_timeout = handle->loop->time + timeout;
if (clamped_timeout < timeout)
clamped_timeout = (uint64_t) -1;
// 初始化回调,超时时间,是否重复计时,赋予一个独立无二的id
handle->timer_cb = cb;
handle->timeout = clamped_timeout;
handle->repeat = repeat;
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;
}
timer_heap
static struct heap *timer_heap(const uv_loop_t* loop) {
#ifdef _WIN32
return (struct heap*) loop->timer_heap;
#else
return (struct heap*) &loop->timer_heap;
#endif
}
timer_less_than
// 两个节点的比较算法
// 按 timeout 从小到大排序,如果时间相等,按 start_id 从小到大排序
static int timer_less_than(const struct heap_node* ha,
const struct heap_node* hb) {
const uv_timer_t* a;
const uv_timer_t* b;
a = container_of(ha, uv_timer_t, heap_node);
b = container_of(hb, uv_timer_t, heap_node);
if (a->timeout < b->timeout)
return 1;
if (b->timeout < a->timeout)
return 0;
/* Compare start_id when both have the same timeout. start_id is
* allocated with loop->timer_counter in uv_timer_start().
*/
return a->start_id < b->start_id;
}
- start 函数首先初始化 handle 里的某些字段,包括超时回调,是否重复启动 定时器、超时的绝对时间等。接着把 handle 节点插入到最小堆中。最后给这 个 handle 打上标记,激活这个 handle
uv__run_timers
- 找出已经超时的节点,并且执行里面的回调
- 源码
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
uv_timer_stop
- 停止一个计时器
- 源码
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; }
- 把 handle 从二叉堆中删除。并且取消激活状态
uv_timer_again
- 重新启动一个计时器,需要设置repeat标记
int uv_timer_again(uv_timer_t* handle) { if (handle->timer_cb == NULL) return UV_EINVAL; // 如果设置了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 的超时时间
uv_timer_set_repeat
- 设置计时器为重复触发
- 源码
void uv_timer_set_repeat(uv_timer_t* handle, uint64_t repeat) { handle->repeat = repeat; }
uv_timer_get_repeat
- 获取计时器的状态——是否为重复触发
- 源码
uint64_t uv_timer_get_repeat(const uv_timer_t* handle) { return handle->repeat; }
uv_timer_get_due_in
- 计时器还有多长时间到时
uint64_t uv_timer_get_due_in(const uv_timer_t* handle) { if (handle->loop->time >= handle->timeout) return 0; return handle->timeout - handle->loop->time; }
timer 在 uv_run 中的使用
- 咱们先来看一下 timer 中的函数在 uv_run 中是如何使用的
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; r = uv__loop_alive(loop); if (!r) uv__update_time(loop); while (r != 0 && loop->stop_flag == 0) { // 更新当前时间 uv__update_time(loop); // 在这里找出超时的节点,运行里面的回调 uv__run_timers(loop); ran_pending = uv__run_pending(loop); uv__run_idle(loop); uv__run_prepare(loop); // ———————————— 下面来讲解这一段 ———————————— timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv__backend_timeout(loop); // 计算要阻塞的时间,开始阻塞 uv__io_poll(loop, timeout); // —————————————————————————————————————————— uv__metrics_update_idle_time(loop); uv__run_check(loop); uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { uv__update_time(loop); uv__run_timers(loop); } r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } if (loop->stop_flag != 0) loop->stop_flag = 0; return r; }
- 首先咱们看一下
epoll_wait
函数,uv__io_poll
就是epoll_wait
的封装#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) events: 用来存内核得到事件的集合, maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size, timeout: 是超时时间 -1: 阻塞 0: 立即返回,非阻塞 >0: 指定毫秒 返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
- 可以看到里面有一个超时时间的参数。意思就是是用户拿一次数据可以等待的时间。 一般我们去使用epoll的时候,如果取不到东西,当然可以无限等待,所以我们平时可能会配成 -1,但是如果在网络库中使用 -1,有可能会造成阻塞,效率大大降低,所以我们要计算出一次最多可以阻塞的时常。
- 计算这个时长的时候,用到了
uv__backend_timeout
函数,所以下面咱们来看一下是如何计算出这个超时时间的// 从 `timer_heap`中得到最小超时的时间, 从而计算出下一次的超时时间 // 阻塞的时长就是最快到期的定时器节点的时长 int uv__next_timeout(const uv_loop_t* loop) { const struct heap_node* heap_node; const uv_timer_t* handle; uint64_t diff; heap_node = heap_min(timer_heap(loop)); if (heap_node == NULL) return -1; /* block indefinitely */ handle = container_of(heap_node, uv_timer_t, heap_node); if (handle->timeout <= loop->time) return 0; diff = handle->timeout - loop->time; if (diff > INT_MAX) diff = INT_MAX; return (int) diff; }