如何在Node.js的长期同步循环中捕获信号?
Node.js同步循环中处理信号与I/O事件的折中方案
问题背景
我写了一段包含同步死循环的代码,发现进入循环后Node.js完全无法处理SIGINT信号或其他I/O事件:
const signals = { INT: false }; process.on("SIGINT", s => { console.log('trapped sigint:', s); signals.INT = true; }); let i = 0; while(true){ if(signals.INT){ console.log('got the SIGINT'); break; } console.log('process.pid:',process.pid, 'next val:', i++); } console.log('exited loop, done'); process.exit(0);
原因很清楚:Node.js的事件循环被同步代码完全阻塞,所有异步任务(包括信号回调)都得不到执行机会。
换成异步循环虽然能解决问题,但循环执行速度会大幅下降:
const signals = { INT: false }; process.on("SIGINT", s => { console.log('trapped sigint:', s); signals.INT = true; }); let i = 0; (async () => { while(true){ if(signals.INT){ console.log('got the SIGINT'); break; } await Promise.resolve(); // 必须await微任务才能让出事件循环 console.log('process.pid:',process.pid, 'next val:', i++); } console.log('exited loop, done'); process.exit(0); })()
我需要一个折中方案:既保留接近同步循环的速度,又能让Node.js及时处理信号和I/O事件。
可行的折中方案
核心思路是批量执行同步迭代,定期主动让出事件循环,让Node.js有机会处理待执行的异步任务。
1. 用process.nextTick实现高速度循环(优先处理微任务)
这种方式每次执行固定数量的同步迭代后,通过process.nextTick调度下一轮循环,开销极小,速度几乎和纯同步循环一致:
const signals = { INT: false }; process.on("SIGINT", s => { console.log('trapped sigint:', s); signals.INT = true; }); let i = 0; const batchSize = 1000; // 可根据需求调整,越大速度越快,响应稍慢 function runBatch() { let count = 0; // 批量执行同步逻辑 while (count < batchSize && !signals.INT) { console.log('process.pid:', process.pid, 'next val:', i++); count++; } // 检查退出条件 if (signals.INT) { console.log('got the SIGINT'); console.log('exited loop, done'); process.exit(0); } // 让出事件循环,处理微任务后继续 process.nextTick(runBatch); } runBatch();
2. 用setImmediate兼顾I/O事件处理
如果你的场景需要同时处理文件、网络等I/O事件,用setImmediate更合适——它会在当前事件循环的I/O阶段结束后调度下一轮循环,确保I/O任务被处理:
const signals = { INT: false }; process.on("SIGINT", s => { console.log('trapped sigint:', s); signals.INT = true; }); let i = 0; const batchSize = 1000; function runBatch() { let count = 0; while (count < batchSize && !signals.INT) { console.log('process.pid:', process.pid, 'next val:', i++); count++; } if (signals.INT) { console.log('got the SIGINT'); console.log('exited loop, done'); process.exit(0); } // 让出事件循环,优先处理I/O事件后继续 setImmediate(runBatch); } runBatch();
关键注意事项
- batchSize的调整:根据业务平衡速度和响应性。比如CPU密集型任务可以把batchSize设大(如10000),需要快速响应信号的场景可以设小(如100)。
- 避免无效await:之前的异步循环中
await console.log没用,因为console.log是同步函数,必须await一个返回Promise的操作(比如Promise.resolve())才能让出事件循环。 - 信号响应的延迟:无论哪种方案,信号回调都要等当前批次执行完才会触发,但只要batchSize不是极大,延迟几乎可以忽略。
内容的提问来源于stack exchange,提问作者Alexander Mills
相关产品推荐
相关产品推荐

