You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript中数千个setTimeout与await如何实现并发运行?

Node.js异步并发现象疑问解答

问题场景

你实现了一套JavaScript客户端-服务端通信应用,客户端向监听localhost:5000的服务端累计发送1000个HTTP请求,单个请求内部通过await等待响应返回。

客户端实现代码

const SendRequest = async () => {
    const res = await fetch("http://localhost:5000")
    return res
}

for(let i=0;i<1000;i++) {
    SendRequest().then(res => console.log(res.status))
    console.log("request sent")
}

客户端运行现象:Node.js执行上述代码时,控制台会先打印1000条request sent日志,间隔一段时间后,才会快速连续打印1000条服务端返回的响应状态日志。

服务端实现代码

app.get("/", async (req, res) => {
    console.log("Connection established");

    setTimeout(() => {
        console.log("Sending res")
        res.send("Hello from the server")
    }, 5000)        // 等待5秒后返回响应
})

服务端运行现象:客户端发起请求后,服务端会先打印1000次Connection established日志;5秒后短短几毫秒内,服务端就会打印1000次Sending res日志,同时1000个响应一次性全部发送并被客户端接收。

核心疑问

已知JavaScript是单线程语言,Node.js会调用额外线程处理setTimeout、HTTP响应等待这类异步操作,测试设备为6核12线程配置:

  • 即便占满系统全部12个线程,Node.js是如何同时等待数千个这类异步操作的?
  • 服务端这1000个延迟均设置为5秒的setTimeout是如何实现并发运行的?

原理解答

首先纠正一个普遍的认知偏差:Node.js处理网络等待、定时器这类异步操作,根本不需要为每个操作分配独立线程。libuv提供的默认大小为4的工作线程池,仅用于处理文件读写、加解密、压缩解压这类需要阻塞执行的任务,网络IO、定时等待的逻辑完全不占用工作线程,和设备的CPU核心/线程数没有直接关系。

网络请求等待的实现逻辑

Node.js的网络IO能力底层依赖操作系统提供的IO多路复用机制(Linux下为epoll、macOS下为kqueue、Windows下为IOCP),整个等待流程完全不需要额外线程参与:

  • 所有发起的网络请求对应的socket连接,都会被注册到操作系统内核的监听列表中
  • Node.js事件循环每次轮询时,只会向内核发起一次系统调用,查询当前哪些连接已经完成数据接收、处于就绪状态
  • 内核将所有就绪的连接列表返回给Node.js后,Node.js才会把对应请求的回调推入任务队列,等待主线程执行
    整个等待响应的过程中,所有连接的状态都由操作系统内核统一维护,既不阻塞Node.js主线程,也不占用libuv工作线程。别说1000个等待中的连接,哪怕是数万、数十万个空闲等待的连接,消耗的CPU和内存资源都极低。
    你观察到客户端先打印1000条request sent,是因为循环中调用SendRequest()时,fetch方法完成请求发送、把socket注册到内核监听列表后就会立刻返回,不会阻塞循环等待响应,因此循环会瞬间执行完成,打印完所有发送日志。等5秒后所有响应同时到达内核,事件循环一次性拿到所有就绪连接,就会连续执行回调打印所有响应状态。

setTimeout定时等待的实现逻辑

setTimeout的实现更轻量,完全在Node.js内部完成,和线程没有任何关系:

  • Node.js内部维护了一个最小堆结构存储所有注册的定时器,堆顶永远是距离触发时间最近的定时器
  • 事件循环每次进入定时器阶段时,只会对比当前时间和堆顶定时器的目标触发时间:
    • 如果堆顶定时器还没到触发时间,就直接进入下一个事件循环阶段,不会空等
    • 如果堆顶定时器已经到期,就依次取出堆中所有到期的定时器,把对应的回调推入任务队列等待主线程执行
      服务端收到1000个请求时,相当于往这个定时器堆里插入了1000个目标触发时间均为“当前时间+5000ms”的定时器。在等待的5秒里,这1000个定时器只是堆中存储的1000条结构化数据,没有任何代码在运行、也没有任何线程被占用。等5秒时间到,事件循环检查定时器堆时发现这1000个定时器全部到期,就会在几毫秒内顺次执行完所有回调,因此你会观察到1000条Sending res日志几乎同时打印,所有响应也同步发回客户端。

本质上,所有“等待”类的异步逻辑都是事件订阅模式:操作发起时只需要注册一个“完成后通知我执行回调”的监听,等待过程中没有任何计算开销,自然不需要多线程支撑高并发。只有需要持续占用CPU执行运算的逻辑,才会消耗CPU线程资源。


内容的提问来源于stack exchange,提问作者Akshay Dagar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 02:54:21