第一章: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的生存之道
异步就不一样了。你点了个外卖,然后继续打游戏,外卖到了再去拿。代码里也是这样——发起一个操作,不等它完成,先干别的。
看这个例子:
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应用,也知道了它的局限性在哪里。
下一章,我们会深入事件循环的内部机制。到时候你会看到,那些回调函数到底是怎么被调度执行的。嗯,挺有意思的。