本文共 4702 字,大约阅读时间需要 15 分钟。
事件循环相信大家都不陌生,很多同学都知道事件循环是一个"死循环",今天我们看一下这个死循环到底是怎样的。我们先看一个朴素版的事件循环系统。
class EventSystem { constructor() { // 任务队列 this.queue = []; } // 追加任务 enQueue(func) { this.queue.push(func); } // 事件循环 run() { while(1) { while(this.queue.length) { const func = this.queue.shift(); func(); } } }}// 新建一个事件循环系统const eventSystem = new EventSystem();// 生产任务eventSystem.enQueue(() => { console.log('hi');});// 启动事件循环eventSystem.run();
以上代码实现了一个非常朴素的事件循环系统
1 新建一个事件循环系统
2 生产任务 3 启动事件循环系统 但是我们发现当没有任务的时候,事件循环系统陷入了死循环,这无疑浪费了cpu。我们看一下执行以上代码的cpu的情况(我电脑4核,可以看到以上代码对应的进程几乎完全占据了一个cpu,1/4)。class EventSystem { constructor() { // 任务队列 this.queue = []; // 是否需要停止任务队列 this.stop = 0; // 超时处理 this.timeoutResolve = null; } // 没有任务时,事件循环的睡眠时间 sleep(time) { return new Promise((resolve) => { let timer = null; // 记录resolve,可能在睡眠期间有任务到来,则需要提前唤醒 this.timeoutResolve = () => { clearTimeout(timer); timer = null; this.timeoutResolve = null; resolve(); }; timer = setTimeout(() => { if (timer) { console.log('timeout'); this.timeoutResolve = null; resolve(); } }, time); }); } // 停止事件循环 setStop() { this.stop = 1; this.timeoutResolve && this.timeoutResolve(); } // 追加任务 enQueue(func) { this.queue.push(func); this.timeoutResolve && this.timeoutResolve(); } // 事件循环 async run() { while(1 && this.stop === 0) { while(this.queue.length) { const func = this.queue.shift(); func(); } // 没有任务了,一直等待(Math.pow(2, 31) - 1为nodejs中定时器的最大值) await this.sleep(Math.pow(2, 31) - 1); } }}// 新建一个事件循环系统const eventSystem = new EventSystem();// 生产任务eventSystem.enQueue(() => { console.log('hi');});// 模拟定时生成一个任务setTimeout(() => { eventSystem.enQueue(() => { console.log('hello'); });}, 1000);// 模拟退出事件循环setTimeout(() => { eventSystem.setStop();}, 2000);// 启动事件循环eventSystem.run();
上面代码的执行结果如下
1 启动事件循环时输出hi。
2 事件循环进入睡眠,1s时被唤醒,输出hello。 3 2s后退出事件循环。 麻雀虽小五脏俱全,以上代码虽然只是个demo,但是已经具备了事件循环的一些核心概念。1 事件循环的整体架构是一个while循环
2 定义任务类型和队列,这里只有一种任务类型和一个队列,比如nodejs里有好几种。
3 没有任务的时候怎么处理?进入睡眠,而不是真的是一个死循环。其中第3点是事件循环系统中非常重要的逻辑。因为事件循环是属于生产者、消费者模式。任务队列中不可能一直都有任务需要处理,这就意味着生产任务可以是一个异步的过程。所以事件循环系统就需要有一种等待的机制。这就会带来两个问题,什么情况下需要等待,什么时候需要退出。这个和具体的业务场景有关,本文实现的事件循环中,没有任务的时候就会一直等待,而不是退出。除非用户手动执行setStop退出。而nodejs中,如果没有actived状态的handle和request并且close阶段没有任务时就会自动退出。另外一个问题就是如何实现等待。这里使用的是setTimeout来模拟睡眠,从而达到等待的效果。但是这时候进程是没有被挂起的,这意味着,我们还可以做其他事情。而在nodejs中,会在poll io阶段,进程会被挂起。我们看看nodejs事件循环的实现。
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__run_check(loop); uv__run_closing_handles(loop); r = uv__loop_alive(loop); }
nodejs的事件循环也是一个while循环,然后在里面执行各个阶段的任务,其中uv__io_poll对应的poll io阶段可能会导致进程挂起。我们看一下uv__io_poll关于等待的逻辑。nfds = epoll_wait(loop->backend_fd, events, ARRAY_SIZE(events), timeout);
epoll_wait会根据timeout的值决定如果没有就绪事件时,是否需要挂起进程。timeout大于0说明是定时器挂起,timeout等于-1说明是永远挂起。直到有就绪队列。这就是nodejs中关于等待的处理逻辑。这和我们自己实现的事件循环系统是类似的,只不过我们是自己唤醒自己,而nodejs中是被操作系统唤醒,因为我们在js层面无法调用操作系统的系统调用挂起进程。epoll是和文件描述符相关的,如果我们不涉及到文件、网络操作,那么我们又如何实现等待呢?我们从libuv的线程池实现中,找到了另一种实现。libuv的线程池中有多个线程,他们共享一个任务队列,每个子线程里不断从共享的任务队列中获取任务处理(需要加锁)。所以这也是一个事件循环的模型。那么当没有任务可处理的时候,libuv是如何实现等待的呢?
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 (;;) { // 没有任务或者某类任务达到阈值 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; } }}
我们看到libuv使用线程库提供的api实现了线程的挂起。从而实现了等待的逻辑。接下来我们看一下唤醒的逻辑。
static void post(QUEUE* q, enum uv__work_kind kind) { uv_mutex_lock(&mutex); QUEUE_INSERT_TAIL(&wq, q); if (idle_threads > 0) uv_cond_signal(&cond); uv_mutex_unlock(&mutex);}
libuv每次提交新的任务到共享队列时,都会判断是否有空闲线程,如果有则唤醒他。
本文介绍了事件循环的设计和实现中涉及到的一些知识,我们看到事件循环的整体架构是类似的,但是具体实现有很多种方式,这取决于你的业务场景。同时,任务的生产是异步的,所以没有任务的时候的等待机制的设计也就变得很重要,我们不能不断地浪费cpu进行轮询,而是要借助一直挂起,唤醒的机制来实现。
转载地址:http://zhcdz.baihongyu.com/