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

Node.js事件循环原理及异步请求阻塞问题的原因与解决

问题描述

我在测试Node.js异步代码时,原本以为有异步操作时Node.js会处理新请求。用Express和Axios写了示例代码:

app.get('/async', (req, res) => {
    axios({
        method: 'get',
        url: 'http://localhost:30000/cpu',
        headers: { 'fibo': '45' }
    })
        .then(ress => {
            console.log(ress)
            res.send('Async success')
        })
        .catch(err => {
            console.log('failed')
            console.log(err)
            res.send('failed')
        })
})
app.get('/', (req, res) => {
    axios({
        method: 'get',
        url: 'http://localhost:30000/'
    }).then(function (response) {
        res.send('Hello World')
    }).catch((err) => {
        res.send('Something wrong happened')
    })
})

请求http://localhost:30000/cpu需要约15秒完成,请求我的服务器/async接口时会调用这个耗时接口,收到响应后返回Async success。我先请求/async,紧接着请求/,原本预期/async处理期间Node.js会同时处理/接口并立即返回,但实际/要等/async完成后才返回,/async阻塞了整个服务器。

请问:

  1. 为何/要等待/async完成?
  2. 该如何解决这个问题?
  3. Node.js事件循环的工作机制及它处理多请求的方式是什么?

一、为什么/会被/async阻塞?

核心原因是你调用的http://localhost:30000/cpu接口本身是在Node.js主线程上执行CPU密集型任务(比如计算斐波那契数列第45项)。

虽然你的Express接口里用了Axios发起异步请求,但如果目标接口/cpu是同一个Node.js服务提供的,且它的处理逻辑是同步的CPU密集型计算,那么这个计算会占用Node.js唯一的主线程。当/async发起请求到/cpu时,/cpu的同步计算会把主线程占满,事件循环无法处理新的请求(包括/接口的请求),直到/cpu的计算完成,主线程空闲下来,才会处理后续的请求。

简单说:不是你的/async接口阻塞了服务器,而是被调用的/cpu接口的同步CPU密集型任务阻塞了Node.js主线程。


二、解决方法

针对CPU密集型任务的阻塞问题,有几种常见解决方案:

1. 将CPU密集型任务移到子进程处理

Node.js提供了child_process模块,可以创建子进程来处理耗时的CPU计算,避免占用主线程。比如修改/cpu接口的处理逻辑:

const { fork } = require('child_process');

app.get('/cpu', (req, res) => {
    const fiboNum = parseInt(req.headers.fibo);
    // 创建子进程执行计算
    const child = fork('./fibo-calculator.js');
    child.send(fiboNum);
    child.on('message', (result) => {
        res.send(result.toString());
        child.kill();
    });
    child.on('error', (err) => {
        res.status(500).send('计算失败');
    });
});

对应的fibo-calculator.js:

process.on('message', (num) => {
    function fibo(n) {
        if (n <= 1) return n;
        return fibo(n - 1) + fibo(n - 2);
    }
    const result = fibo(num);
    process.send(result);
});

子进程独立于主线程运行,计算时不会阻塞主线程的事件循环,其他请求可以正常处理。

2. 使用Worker Threads(Node.js 10.5+)

Worker Threads是专门为CPU密集型任务设计的,比子进程更轻量,共享内存空间:

const { Worker } = require('worker_threads');

app.get('/cpu', (req, res) => {
    const fiboNum = parseInt(req.headers.fibo);
    const worker = new Worker('./fibo-worker.js', { workerData: fiboNum });
    
    worker.on('message', (result) => {
        res.send(result.toString());
        worker.terminate();
    });
    
    worker.on('error', (err) => {
        res.status(500).send('计算失败');
    });
});

fibo-worker.js:

const { workerData, parentPort } = require('worker_threads');

function fibo(n) {
    if (n <= 1) return n;
    return fibo(n - 1) + fibo(n - 2);
}

const result = fibo(workerData);
parentPort.postMessage(result);

3. 横向扩展多实例

如果单进程处理能力不足,可以用cluster模块启动多个Node.js进程,利用多核CPU资源:

const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
const express = require('express');

if (cluster.isPrimary) {
    console.log(`主进程 ${process.pid} 正在运行`);
    // 衍生工作进程
    for (let i = 0; i < numCPUs; i++) {
        cluster.fork();
    }
    cluster.on('exit', (worker, code, signal) => {
        console.log(`工作进程 ${worker.process.pid} 退出,重新启动`);
        cluster.fork();
    });
} else {
    const app = express();
    // 你的路由和逻辑
    app.get('/cpu', (req, res) => {
        // 原来的计算逻辑,但现在每个进程处理自己的请求,不会全部阻塞
        function fibo(n) {
            if (n <= 1) return n;
            return fibo(n - 1) + fibo(n - 2);
        }
        const result = fibo(parseInt(req.headers.fibo));
        res.send(result.toString());
    });
    app.listen(30000);
    console.log(`工作进程 ${process.pid} 已启动`);
}

这样不同的请求会被分配到不同的工作进程,单个进程被阻塞时,其他进程仍能处理请求。


三、Node.js事件循环工作机制及多请求处理方式

1. 事件循环核心原理

Node.js是单线程的,但通过事件循环实现了异步非阻塞IO。主线程负责处理事件循环,所有的异步操作(比如网络请求、文件IO)都会被委托给底层的libuv库(或操作系统)处理,当异步操作完成后,对应的回调会被放入事件队列,等待主线程空闲时执行。

事件循环分为几个按顺序执行的阶段:

  • timers:执行setTimeout和setInterval的回调
  • pending callbacks:执行延迟到下一次循环的IO回调
  • idle, prepare:内部使用的阶段
  • poll:处理IO事件(网络、文件等),是最主要的阶段,如果没有timer要处理,会在这里等待新的IO事件
  • check:执行setImmediate的回调
  • close callbacks:执行关闭事件的回调(比如socket.on('close', ...))

每个阶段执行完该阶段所有的回调后,才会进入下一个阶段。

2. 多请求处理方式

当Node.js的HTTP服务器收到请求时,会把请求的处理逻辑(你的Express路由回调)加入事件队列。主线程在事件循环的poll阶段会取出这些请求回调执行:

  • 如果回调里是异步IO操作(比如Axios请求、文件读写),主线程会把操作委托给底层,然后继续处理下一个请求回调,不会等待异步操作完成。当异步操作完成后,回调会被放入队列,等待主线程空闲时执行。
  • 如果回调里是CPU密集型的同步代码,主线程会一直执行这段代码,直到完成,期间无法处理其他请求,这就是阻塞。

回到你的问题:/cpu接口的同步计算是CPU密集型任务,占用了主线程,导致事件循环无法处理/接口的请求,直到/cpu的计算完成,主线程空闲,才会处理/的请求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 19:43:13