跳转至内容
0
  • 主页
  • 版块
  • 最新
  • 标签
  • 热门
  • 主页
  • 版块
  • 最新
  • 标签
  • 热门
折叠
品牌标识
让每一次思考都有价值
  1. 让每一次思考都有价值
  2. 版块
  3. 记录与分享
  4. Node.js的单线程模型和非阻塞I/O是如何工作的

Node.js的单线程模型和非阻塞I/O是如何工作的

已定时 已固定 已锁定 已移动 记录与分享
nodejs
1 评论 1 发布者 0 浏览
  • 从旧到新
  • 从新到旧
  • 最多赞同
评论
  • 在新文章中评论
登录后评论
此文章已被删除。只有拥有文章管理权限的用户可以查看。
  • 首席贫困代表首 离线
    首席贫困代表首 离线
    首席贫困代表
    编写于 最后由 编辑
    #1
    目录
    一、单线程模型:并非“只有一个线程”
    1. 主线程(JavaScript 执行线程)
    2. 其他辅助线程(由 libuv 管理)
    总结:单线程模型的本质
    二、非阻塞 I/O:主线程从不“等待”
    1. 主线程发起 I/O 请求,立即“放手”
    2. libuv 调度 I/O 任务(交给操作系统或线程池)
    3. I/O 完成后,回调函数进入“事件队列”
    4. 事件循环(Event Loop)调度回调执行
    三、单线程 + 非阻塞 I/O 的协同优势
    四、局限性:不适合 CPU 密集型任务
    总结

    Node.js 的“单线程模型”与“非阻塞 I/O”是其高并发能力的核心,二者相辅相成。理解它们的工作机制,能帮助我们明白为什么 Node.js 能高效处理大量 I/O 密集型任务(如网络请求、文件读写)。

    一、单线程模型:并非“只有一个线程”

    Node.js 的“单线程”特指主线程(JavaScript 执行线程)是单线程,而非整个 Node.js 运行时只有一个线程。其核心构成包括:

    1. 主线程(JavaScript 执行线程)

    • 作用:执行 JavaScript 代码(同步代码、回调函数),管理调用栈(Call Stack)和事件队列(Callback Queue)。

    • 特点:同一时间只能执行一段代码(单线程特性),遵循“先进后出”的调用栈规则。

      例如,执行以下代码时,主线程会按顺序将函数压入栈中执行:

      function a() { console.log('a'); }
      function b() { a(); console.log('b'); }
      b(); 
      // 调用栈过程:压入 b() → 压入 a() → 执行 a() 并弹出 → 执行 b() 剩余代码并弹出
      

    2. 其他辅助线程(由 libuv 管理)

    Node.js 底层依赖 libuv(跨平台异步 I/O 库),它会创建一组辅助线程(默认 4 个,可通过 UV_THREADPOOL_SIZE 调整),负责处理两类任务:

    • 阻塞 I/O 操作:如文件读写(fs.readFile)、数据库查询(如 MySQL 查询)等。
    • CPU 密集型任务:如数据加密(crypto 模块)、压缩(zlib 模块)等。

    这些辅助线程独立于主线程,不会阻塞 JavaScript 代码的执行。

    总结:单线程模型的本质

    “单线程”是指JavaScript 代码的执行由一个主线程负责,但 I/O 操作和 CPU 密集型任务会被“外包”给 libuv 的线程池处理。这种设计避免了多线程切换的开销(传统后端语言的性能瓶颈之一),同时通过辅助线程弥补了单线程的能力局限。

    二、非阻塞 I/O:主线程从不“等待”

    “非阻塞 I/O”是指:当主线程发起 I/O 操作(如读取文件、发送网络请求)时,不会暂停等待操作完成,而是继续执行后续代码;当 I/O 操作完成后,其结果会通过“回调函数”通知主线程处理。

    其工作流程可分为 4 步:

    1. 主线程发起 I/O 请求,立即“放手”

    当代码中遇到 I/O 操作(如 fs.readFile),主线程会:

    • 将 I/O 任务封装成“请求对象”,包含操作参数(如文件路径)和回调函数(操作完成后执行的逻辑)。

    • 把这个请求对象交给 libuv 处理,自己则继续执行后续同步代码(不等待 I/O 完成)。

      示例:

      console.log('开始');
      // 发起 I/O 操作(读取文件)
      fs.readFile('./test.txt', (err, data) => {
        if (err) throw err;
        console.log('文件内容:', data.toString());
      });
      console.log('继续执行'); 
      // 输出顺序:开始 → 继续执行 → 文件内容: ...(I/O 完成后)
      

    2. libuv 调度 I/O 任务(交给操作系统或线程池)

    libuv 接收请求对象后,会根据 I/O 类型选择处理方式:

    • 非阻塞 I/O 支持的操作(如网络请求、部分文件操作):直接调用操作系统的非阻塞 I/O 接口(如 Linux 的 epoll、Windows 的 IOCP),由操作系统内核处理,完成后通知 libuv。
    • 阻塞 I/O 或 CPU 密集型操作(如某些数据库驱动、压缩):交给 libuv 的线程池处理,线程池中的线程执行任务,完成后通知 libuv。

    3. I/O 完成后,回调函数进入“事件队列”

    当 I/O 操作完成(无论成功/失败),libuv 会将对应的回调函数(如 fs.readFile 的匿名函数)放入事件队列(Callback Queue) 等待执行。

    事件队列是一个“先进先出”的队列,存放所有待执行的回调函数(包括 I/O 回调、定时器回调、事件回调等)。

    4. 事件循环(Event Loop)调度回调执行

    主线程在完成所有同步代码后,会进入事件循环,不断从事件队列中取出回调函数,压入调用栈执行。

    事件循环的核心逻辑是:“同步代码执行完毕 → 处理事件队列中的回调 → 重复此过程”。

    其具体阶段(按顺序)如下(简化版):

    1. 处理定时器回调(setTimeout/setInterval)。
    2. 处理 I/O 回调(如 fs.readFile、网络请求的回调)。
    3. 处理 setImmediate 回调(专门设计在 I/O 回调后执行)。
    4. 处理关闭事件回调(如 socket.on('close', ...))。

    形象比喻:
    主线程是“餐厅服务员”,同步代码是“当前顾客的点餐”,I/O 操作是“让后厨做餐”,事件队列是“已做好的餐品队列”,事件循环是“服务员不断检查队列,把做好的餐端给顾客”。服务员(主线程)不会站在后厨等餐(不阻塞),而是继续接待其他顾客(执行同步代码),餐做好后再端上来(执行回调)。

    三、单线程 + 非阻塞 I/O 的协同优势

    这种模型的核心优势在于高效利用主线程,尤其适合 I/O 密集型场景:

    • 无线程切换开销:单线程避免了多线程间的上下文切换(传统后端语言的性能杀手),主线程可专注处理回调。
    • 高并发支持:一个主线程通过事件循环,能同时处理数万甚至数十万 I/O 请求(因为 I/O 操作主要由操作系统或线程池处理,主线程仅负责调度)。
    • 资源利用率高:非阻塞 I/O 让主线程“不空闲”,始终在处理有意义的工作(同步代码或回调)。

    四、局限性:不适合 CPU 密集型任务

    单线程模型的短板在于无法高效处理 CPU 密集型任务(如大数组排序、复杂计算):

    • 若主线程执行长时间运行的同步代码(如 for (let i = 0; i < 1e9; i++) {}),会阻塞事件循环,导致事件队列中的回调无法执行(请求超时、界面卡顿)。
    • 虽然 libuv 线程池可分担部分 CPU 任务,但线程池大小有限(默认 4 个),大量 CPU 任务会排队等待,仍会成为瓶颈。

    总结

    Node.js 的“单线程模型”指 JavaScript 执行依赖一个主线程,而“非阻塞 I/O”通过 libuv 将 I/O 任务交给操作系统或线程池处理,再通过事件循环调度回调执行。二者结合,让 Node.js 能以极低的资源消耗处理海量 I/O 请求,成为 API 服务、实时应用等场景的理想选择。但需注意避开 CPU 密集型任务,或通过多进程(cluster 模块)等方式弥补短板。

    1 条评论 最后评论
    0
    评论
    • 在新文章中评论
    登录后评论
    • 从旧到新
    • 从新到旧
    • 最多赞同


    • 登录

    • 没有帐号? 注册

    • 登录或注册以进行搜索。
    • 第一个评论
      最后一个评论