Eventloop 学习总结

本文详细探讨了JavaScript的单线程非阻塞特性及其背后的事件循环机制,包括执行栈、事件队列、宏任务和微任务的概念,以及浏览器和Node.js中事件循环的不同实现。

产生背景

javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互。

单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。

而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。

单线程是必要的,也是javascript这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作。试想一下 如果javascript是多线程的,那么当两个线程同时对dom进行一项操作,例如一个向其添加事件,而另一个删除了这个dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。

前面提到javascript的另一个特点是“非阻塞”,那么javascript引擎到底是如何实现的这一点呢?答案就是今天这篇文章的主角——event loop(事件循环)。

由于浏览器和nodejs在实现eventloop时机制有不同之处,所以将分开讲述这两种实现方法

浏览器的Eventloop

Eventloop事件循环中的执行栈事件队列宏任务微任务等概念需要首先理清

什么是执行栈

执行栈

执行栈就是js代码运行的地方,上图call stack所示。

什么是事件队列

当浏览器中的事件监听函数被触发(DOM)、网络请求的相应(ajax)、定时器被触发(setTimeout)相对应的回调函数就会被推送到事件队列中,等待执行;如上图中的Callback Queue。

什么是宏队列

script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering

什么是微任务

process.nextTick(Nodejs), Promises, Object.observe, MutationObserver;

事件循环(event-loop)是什么?

事件循环Eventloop

宏队列与微任务的执行顺序宏队列与微任务的执行顺序

具体来说,浏览器会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行,以此类推。

例子说明一切

在这里插入图片描述

首先,全局代码(main())压入调用栈执行,打印start

接下来setTimeout压入macrotask队列,promise.then回调放入microtask队列,最后执行console.log(‘end’),打印出end

至此,调用栈中的代码被执行完成,回顾macrotask的定义,我们知道全局代码属于macrotask,macrotask执行完,那接下来就是执行microtask队列的任务了,执行promise回调打印promise1

promise回调函数默认返回undefined,promise状态变为fullfill触发接下来的then回调,继续压入microtask队列,event loop会把当前的microtask队列一直执行完,此时执行第二个promise.then回调打印出promise2

这时microtask队列已经为空,从上面的流程图可以知道,接下来主线程会去做一些UI渲染工作(不一定会做),然后开始下一轮event loop,执行setTimeout的回调,打印出setTimeout

这个过程会不断重复,也就是所谓的事件循环

async/await

async 做一件什么事情?

带 async 关键字的函数,它使得你的函数的返回值必定是 promise 对象

也就是

如果async关键字函数返回的不是promise,会自动用Promise.resolve()包装

如果async关键字函数显式地返回promise,那就以你返回的promise为准

await 在等什么?

await等的是右侧「表达式」的结果

也就是说,

右侧如果是函数,那么函数的return值就是「表达式的结果」

右侧如果是一个 ‘hello’ 或者什么值,那表达式的结果就是 ‘hello’

async function async1() {
    console.log( 'async1 start' )
    await async2()
    console.log( 'async1 end' )
}
async function async2() {
    console.log( 'async2' )
}
async1()
console.log( 'script start' )

这里注意一点,可能大家都知道await会让出线程,阻塞后面的代码,那么上面例子中, ‘async2’ 和 ‘script start’ 谁先打印呢?

是从左向右执行,一旦碰到await直接跳出, 阻塞async2()的执行?

还是从右向左,先执行async2后,发现有await关键字,于是让出线程,阻塞代码呢?

实践的结论是,从右向左的。先打印async2,后打印的script start

await 等到之后,做了一件什么事情?

那么右侧表达式的结果,就是await要等的东西。

等到之后,对于await来说,分2个情况

  • 不是promise对象
  • 是promise对象

如果不是 promise , await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果

如果它等到的是一个 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。

注意:

在async/await的处理上谷歌70和73实现是不同,具体可以看这篇文章

Node JS 的Eventloop实现

在这里插入图片描述

  • timers 阶段:这个阶段执行timer(setTimeoutsetInterval)的回调
  • I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
  • idle, prepare 阶段:仅node内部使用
  • poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check 阶段:执行 setImmediate() 的回调
  • close callbacks 阶段:执行 socketclose 事件回调

timers 阶段

timers 是事件循环的第一个阶段,Node 会去检查有无已过期的timer,如果有则把它的回调压入timer的任务队列中等待执行,事实上,Node 并不能保证timer在预设时间到了就会立即执行,因为Node对timer的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout()setImmediate() 的执行顺序是不确定的。

setTimeout(() => {
    console.log('setTimeout');
}, 0);
setImmediate(() => {
    console.log('setImmediate');
})

执行的结果是这样子的:

setTimeout和setImmediate比较

首先进入的是timers阶段,如果我们的机器性能一般,那么进入timers阶段,一毫秒已经过去了(setTimeout(fn, 0)等价于setTimeout(fn, 1)),那么setTimeout的回调会首先执行。

如果没有到一毫秒,那么在timers阶段的时候,下限时间没到,setTimeout回调不执行,事件循环来到了poll阶段,这个时候队列为空,此时有代码被setImmediate(),于是先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout的回调函数。

而我们在执行代码的时候,进入timers的时间延迟其实是随机的,并不是确定的,所以会出现两个函数执行顺序随机的情况。

再对比一段代码:

var fs = require('fs')

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});

这里我们就会发现,setImmediate永远先于setTimeout执行。

原因如下:

fs.readFile的回调是在poll阶段执行的,当其回调执行完毕之后,poll队列为空,而setTimeout入了timers的队列,此时有代码被setImmediate(),于是事件循环先进入check阶段执行回调,之后在下一个事件循环再在timers阶段中执行有效回调。

poll 阶段

poll 阶段主要有2个功能:

  • 处理 poll 队列的事件
  • 当有已超时的 timer,执行它的回调函数

even loop将同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限(上限具体多少未详),接下来even loop会去检查有无预设的setImmediate(),分两种情况:

  1. 若有预设的setImmediate(), event loop将结束poll阶段进入check阶段,并执行check阶段的任务队列
  2. 若没有预设的setImmediate(),event loop将阻塞在该阶段等待

注意一个细节,没有setImmediate()会导致event loop阻塞在poll阶段,这样之前设置的timer岂不是执行不了了?所以咧,在poll阶段event loop会有一个检查机制,检查timer队列是否为空,如果timer队列非空,event loop就开始下一轮事件循环,即重新进入到timer阶段。

check 阶段

setImmediate()的回调会被加入check队列中, 从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。

Nodejs的Eventloop小结

  • event loop 的每个阶段都有一个任务队列
  • 当 event loop 到达某个阶段时,将执行该阶段的任务队列,直到队列清空或执行的回调达到系统上限后,才会转入下一个阶段
  • 当所有阶段被顺序执行一次后,称 event loop 完成了一个 tick

一个例子

在这里插入图片描述

首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2

至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1promise2

<think>好的,用户之前询问了muduo库的客户端底层实现,现在他进一步想详细了解EventLoop类。我需要先回顾之前的对话,确保这次回答与之前的连贯。用户可能对muduo库有一定的了解,正在深入学习其内部机制,特别是事件循环部分。 用户的需求是详细了解EventLoop类,可能需要了解它的核心功能、结构、关键成员以及运作机制。由于用户之前已经接触过一些muduo的基础知识,这次应该深入一些,但又要避免过于晦涩的代码细节,保持解释的清晰性。 首先,我需要确定EventLoop类在muduo中的作用,它是事件循环的核心,负责处理IO事件、定时器和回调。然后要分点介绍,比如核心职责、关键成员、工作流程、线程模型、跨线程调用、唤醒机制、定时器管理、异常处理等。 用户可能想知道EventLoop如何与Poller、Channel协作,如何处理定时任务,如何确保线程安全,以及如何唤醒事件循环。需要强调one loop per thread的设计,每个线程一个EventLoop实例,避免竞争条件。 还要考虑用户可能的疑问点,比如如何实现跨线程调用函数,唤醒机制为什么使用eventfd而不是管道,定时器如何管理,这些都需要详细说明。同时,要指出常见的陷阱,比如确保函数在正确的线程执行,避免死锁。 最后,总结部分需要强调EventLoop的重要性,以及在网络编程中的实际应用建议。可能用户需要将这些知识应用到实际项目中,所以给出源码位置和调试建议会有帮助。</think>muduo的EventLoop类是网络库最核心的Reactor实现,其设计体现了"one loop per thread"的核心理念。以下是该类的深度解析: --- ### 一、核心职责 1. **IO事件分发** - 通过Poller(epoll/poll)监控文件描述符事件 - 采用Level Trigger模式处理事件 - 每个Channel对象对应一个文件描述符 2. **任务调度** - 处理定时器任务(TimerQueue) - 执行跨线程投递的异步任务 - 维护待处理回调队列 3. **生命周期管理** - 控制事件循环的启动/停止 - 管理所属线程的上下文 --- ### 二、关键成员解析 ```cpp class EventLoop { private: std::unique_ptr<Poller> poller_; // 多态IO复用器 int wakeupFd_; // 用于唤醒的事件fd Channel wakeupChannel_; // 唤醒通道 TimerQueue timerQueue_; // 定时器管理 std::vector<Functor> pendingFunctors_; // 待执行回调队列 // ... }; ``` --- ### 三、核心工作流程 1. **事件循环启动** ```cpp void EventLoop::loop() { while (!quit_) { activeChannels_.clear(); pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_); // 处理IO事件 for (Channel* channel : activeChannels_) { channel->handleEvent(pollReturnTime_); } // 处理异步任务 doPendingFunctors(); } } ``` 2. **定时器管理机制** - 内部使用`timerfd_create`实现定时器 - 采用红黑树存储定时任务(O(logN)复杂度) - 支持精准的重复定时器(cron风格) 3. **跨线程调用实现** ```cpp void EventLoop::runInLoop(Functor cb) { if (isInLoopThread()) { cb(); // 直接执行 } else { queueInLoop(std::move(cb)); // 跨线程投递 } } ``` --- ### 四、重要技术细节 1. **线程模型** - 每个EventLoop实例严格绑定创建线程 - 通过`threadId_`成员校验线程归属 - 使用`assertInLoopThread()`确保线程安全 2. **唤醒机制** - 使用`eventfd`而非传统管道(更高效) - 写入8字节数据触发唤醒 - 通过`wakeupChannel_`处理唤醒事件 3. **定时器精度控制** - 采用`CLOCK_MONOTONIC`时钟源 - 最小时间间隔1微秒(实际受内核调度影响) - 通过`TimerQueue::resetTimerfd`重置定时器 --- ### 五、典型使用场景 1. **网络IO处理** ```cpp // 注册socket事件 channel->setReadCallback([&](Timestamp t){ handleRead(t); }); channel->enableReading(); ``` 2. **定时任务调度** ```cpp loop->runEvery(5.0, []{ LOG_INFO << "Periodic task every 5s"; }); ``` 3. **跨线程任务投递** ```cpp // 在其他线程向EventLoop提交任务 other_thread([loop=eventLoop]{ loop->runInLoop([]{ // 安全操作EventLoop资源 }); }); ``` --- ### 六、性能优化要点 1. **缓冲区设计** - 每个Channel维护独立的事件处理上下文 - 使用分散/聚集IO(readv/writev)提升性能 2. **延迟唤醒策略** - 合并多个唤醒请求为单次事件处理 - 通过`callingPendingFunctors_`标志位控制 3. **对象生命周期管理** - 使用`shared_ptr`和`weak_ptr`管理跨线程对象 - 通过`bind`捕获上下文实现安全回调 --- ### 七、调试与陷阱 1. **常见问题** - 未正确处理`EINTR`导致的循环退出 - 回调函数执行时间过长阻塞事件循环 - 跨线程访问未加锁导致的竞态条件 2. **调试技巧** - 通过`TRACE`宏跟踪事件循环状态 - 使用`valgrind --tool=helgrind`检测线程问题 - 打印`eventLoop->name()`确认线程归属 --- ### 源码定位建议 - **核心实现文件**: muduo/net/EventLoop.cc - **关键辅助类**: - Poller(封装epoll) - TimerQueue(定时器管理) - Channel(文件描述符包装) 该类的设计体现了Reactor模式的高效实现,理解EventLoop的工作机制是掌握muduo库的关键。在实际网络编程中,要特别注意避免在非IO线程操作EventLoop相关对象。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值