第一章:Node.js异步编程基石

同步与阻塞:你被卡住了

先聊聊同步。说白了,同步就是一件事做完才能做下一件。你排队买奶茶,前面的人没付钱,你就得等着。代码里也一样——函数调用没返回,后面的代码就别想跑。

我刚开始写Node.js时,写过一段这样的代码:

const fs = require('fs');
const data = fs.readFileSync('/big-file.txt', 'utf8');
console.log('文件读完了');
console.log('这行代码得等上面读完才能执行');

这就是同步。readFileSync没读完,后面的console.log就卡着。阻塞,就是这个意思——当前线程被占用了,啥也干不了。

注意: 阻塞不一定是坏事。但如果你在Node.js主线程里做同步I/O,那整个应用就瘫痪了。我曾经在线上环境见过一个同事,用readFileSync读了个几百MB的日志文件,结果所有请求都卡住了。嗯,那场面,挺尴尬的。

异步与非阻塞:Node.js的生存之道

异步就不一样了。你点了个外卖,然后继续打游戏,外卖到了再去拿。代码里也是这样——发起一个操作,不等它完成,先干别的。

看这个例子:

const fs = require('fs');
fs.readFile('/big-file.txt', 'utf8', (err, data) => {
  console.log('文件读完了');
});
console.log('这行代码不用等,直接执行');

readFile调用后立刻返回,回调函数等文件读完才执行。这就是非阻塞——调用不阻塞后续代码执行。

你想想看,如果每个请求都要等数据库查询、文件读取、网络请求,那服务器能扛几个并发?

为什么Node.js选择异步模型?

这个问题我面试时经常问。答案其实很简单:Node.js是单线程的

单线程意味着什么?一个进程只有一个主线程。如果这个线程被阻塞了,整个应用就停了。所以Node.js必须用异步非阻塞模型,否则根本没法用。

我做过一个对比实验:用同步方式处理100个并发请求,每个请求读一个10MB文件。结果呢?最后一个请求等了将近30秒。换成异步方式,全部请求在2秒内完成。差距就是这么夸张。

核心要点:

  • 同步阻塞:线程等待,资源浪费
  • 异步非阻塞:线程不等待,继续处理其他任务
  • Node.js选择异步模型,是因为单线程必须这么做

异步模型带来的好处

说白了,异步模型让Node.js用很少的资源处理大量并发。一个8GB内存的服务器,用Java可能只能扛几千个连接,用Node.js可以轻松扛几万甚至十几万。

为什么会这样?因为异步模型不需要为每个连接创建一个线程。线程是很贵的——每个线程都要分配栈空间、上下文切换也有开销。而Node.js的事件循环机制,用一个线程就能处理成千上万的并发连接。

我记得有一次帮客户优化一个API网关。原来用Python写的,200个并发就撑不住了。我改成Node.js重写,同样的硬件,跑到5000并发还很稳。客户当时就震惊了。

避坑指南:异步不是万能药

我曾经犯过一个错误:在回调里做CPU密集计算。比如这样:

fs.readFile('/data.json', (err, data) => {
  // 这里做大量计算,比如解析超大JSON
  const result = JSON.parse(data); // 如果data有几百MB,这里会卡住
  // 后续处理...
});

异步只解决了I/O等待的问题。CPU密集计算还是会阻塞事件循环。所以Node.js适合I/O密集场景,不适合CPU密集场景。如果你要做图像处理、视频编码、大量数学计算,建议用Worker Threads或者干脆换语言。

我的建议:

  • I/O操作(文件、网络、数据库)用异步
  • CPU密集操作用Worker Threads或子进程
  • 永远不要在事件循环里做耗时计算

总结一下

同步阻塞和异步非阻塞,说白了就是两种做事方式。Node.js选择异步模型,不是因为它更高级,而是因为单线程必须这么干。理解了这个,你就能明白为什么Node.js适合做高并发I/O应用,也知道了它的局限性在哪里。

下一章,我们会深入事件循环的内部机制。到时候你会看到,那些回调函数到底是怎么被调度执行的。嗯,挺有意思的。