AI → Full-Stack
365 天全栈成长 · PHASE 1 · WEEK 1

Day 1 - 服务端是怎么同时接待很多人的

今日时长:3 小时(学习 50 min + 动手 100 min + 整理 30 min)· 主题:并发模型(Concurrency Models)——后端世界的第一块基石 · 练习语言:Node.js(它只是教具,概念才是主角)

🍜 从一个面馆说起:为什么后端要谈「并发」

你开了家面馆,饭点一到,门口同时挤进来 100 个客人。你只有一个灶台。怎么办?

后端服务器每天面对的就是这个问题:成千上万的请求(客人)同时涌进来,而 CPU、内存(灶台、服务员)是有限的。「并发模型」就是一套「开店方案」——怎么安排人手,让尽量多的客人尽快吃上面,还不出乱子(上错菜、厨房着火)。

💡 生活翻译: 你在浏览器里点开一个网页,背后可能有几百台服务器在接力。你今天学的不是"Node.js 怎么写",而是所有后端语言共同的一道必答题:人手有限,客人无限,怎么调度?

📇 概念卡:并发(Concurrency)≠ 并行(Parallelism)

概念卡 #1

并发(Concurrency):同一时间段内应付多件事——一件事没做完可以先搁下,去做另一件。像一个人一边回微信一边烧水,水开了先去关火。

并行(Parallelism):同一时刻真的同时做多件事——需要多个人(多个 CPU 核心)各干各的。

一句话记:并发是「会切换」,并行是「真同时」。并发是组织结构问题,并行是人手数量问题。

这个概念不绑定任何语言。C#、Java、Go、Node 的面试里都会考,而且考法一模一样。

👥 三种开店方案 = 三种并发模型

全世界的主流后端语言,应对"100 个客人同时进门"基本就三种思路。记住这三种,你就抓住了所有后端运行时的骨架:

方案一:多雇服务员 —— 多线程模型(Java / C# 的传统做法)

每个客人配一个专职服务员(一个请求一个线程)。服务员点完单就站着等后厨出餐,哪儿也不去。

好处:思路直白,代码从上往下写,好理解。
代价:服务员很贵!每个线程要占内存(约 1MB 栈空间),1 万个客人就要 1 万个服务员,餐馆装不下;服务员之间换岗(线程上下文切换)也费时间。

现代做法是用线程池(Thread Pool):固定雇 200 个服务员轮班,而不是来多少客人招多少人。C# 的 async/await、Java 的 virtual threads 都是在给这个方案"减肥"。

方案二:一个永不等人的超级服务员 —— 事件循环模型(Node.js)

只雇一个服务员,但他有条铁律:绝不站着等。给 1 号桌点完单,单子递给后厨,立刻去 2 号桌。后厨出餐了按铃(事件),他听到铃声再把菜端过去。

这个服务员就是 event loop(事件循环);后厨是操作系统/线程池在干慢活(读文件、查数据库、发网络请求);按铃就是回调(callback)

好处:一个人能"应付"上万桌,因为大部分桌 90% 的时间都在等后厨,根本不需要人陪着。
代价:如果有一桌客人拉着服务员现场算一道奥数题(CPU 密集计算),全场都别想点菜了。

方案三:雇得起一万个实习生 —— 轻量协程模型(Go)

Go 的 goroutine 像"实习生":工资极低(初始栈只有几 KB),雇 10 万个也不心疼。每个实习生也像方案一那样专职服务一桌,写代码同样是从上往下的直白风格,但成本接近方案二。

一句话:Go 用很便宜的人手,实现了"多服务员"的简单写法 + "事件循环"的高容量。

概念卡 #2

并发模型(Concurrency Model):一门语言的运行时用来"同时应付多个任务"的组织方案。三大流派:多线程 + 线程池(Java/C#)、单线程事件循环 + 异步回调(Node.js)、轻量级协程(Go)。它们解决的问题相同,差别在人手成本代码写法

🔁 放大看 Node.js:event loop 是怎么转起来的

Node.js 内部就三个关键角色,用面馆来认人:

面馆角色Node.js 里的真身干什么
服务员(全场唯一)Call Stack(调用栈)+ event loop执行你的 JS 代码,一圈一圈不停地巡视:有没有铃响?有没有活干?
等出餐的取餐铃区Callback Queue(回调队列)后厨完成的事(定时器到点、文件读完)在这里排队,等服务员来处理
后厨帮工libuv 线程池真正干慢活:读文件、DNS 查询、加密等。注意:Node 并不是"绝对单线程",只是你的代码跑在单个线程上

event loop 的每一圈就像服务员的一次巡视,大致顺序:先看定时器到点没(timers)→ 处理排队的回调(poll)→ 有没有 setImmediate(check)……下周我们会逐阶段拆实验。今天只需要建立画面感:服务员永不停歇地绕圈,绝不站在任何一桌旁边干等。

⚠️ 黄金法则:别让服务员站在原地等

💡 生活翻译: 面馆里最可怕的客人不是点 100 碗面的,而是拉着服务员陪他现场磨面粉的。磨面粉的 5 分钟里,全场 100 桌没人点菜、没人结账——服务器对外表现为"卡死"。

对应到后端,任务分两类:

类型例子面馆类比正确做法
I/O 密集(等待型)查数据库、读文件、调别的 API等后厨出餐交给"后厨",用异步;event loop 模型最擅长这种
CPU 密集(计算型)图片压缩、大 JSON 解析、加密、复杂循环现场磨面粉别在主线程做!拆小、或丢给 worker_threads(另雇一个磨粉工)

这条法则在 C# 里同样成立,只是表现不同:在 C#/Java 里堵住一个线程,只是占用一个服务员;在 Node 里堵住主线程,是放倒了唯一的服务员。所以今天起养成职业直觉:看到一段代码,先问"它是等待型还是计算型?"

🌍 跨语言对照(以后换 C# 也不怕)

通用概念Node.js(今天的教具)C# / .NETJavaGo
并发模型event loop + libuv 线程池线程池 + async/await(Task)线程池 / virtual threads(Loom)goroutine + channel
"别堵住服务员"别阻塞主线程别阻塞线程池线程(用 await 让出)同左goroutine 阻塞了调度器会换人,代价小
CPU 密集任务出路worker_threadsTask.Run / 多线程线程池 / ForkJoinPool多开 goroutine 天然多核
异步代码写法callback → Promise → async/awaitasync/await(语法几乎一样)CompletableFuture / 协程(Loom 后回归同步写法)写法本身就是同步风格

📌 面试翻译练习:如果面试官问 ".NET 的 async/await 和 Node 的有什么区别?",你的答案是:"写法几乎相同,差别在底层——C# 的 await 让出的是线程池里的一个线程,Node 的 await 让出的是唯一的主线程,所以 Node 对阻塞更敏感。" 这就是"学概念而不是学语言"的回报。

🛠️ 今日习题(约 30 min)

习题代码写在 src/01-concurrency-models/ 目录下,我已放好带中文注释的题目骨架,你来填关键部分。运行方式:npx tsx src/01-concurrency-models/ex1-blocking.ts

习题 1:抓住"磨面粉的客人"(动手,约 12 min)

文件:ex1-blocking.ts

同时安排一个 1 秒后该响的"取餐铃"(setTimeout)和一盘要磨 3 秒的面粉(一个空的 while 大循环)。运行后观察:铃声迟到了多久?把观察结果写进代码顶部注释——这就是"主线程被堵,全场停摆"的直接证据。

习题 2:服务员不等待(动手,约 10 min)

文件:ex2-io-waiting.ts

分别用同步(readFileSync)和异步(fs/promisesreadFile)读一个大文件,各包一层计时。体会:同步版里服务员站在原地等后厨;异步版里他递完单子就去干别的了。想想:这正是 C# 里 File.ReadAllText vs await File.ReadAllTextAsync 的同一道题。

习题 3:概念卡默写(动笔不动键盘,约 8 min)

文件:notes.md(放在同一目录)

合上本文档,用中文回答两题(每题不超过 5 句话,必须用自己的话):

  1. 并发和并行有什么区别?举面馆的例子说明。
  2. 为什么 Node.js 特别怕 CPU 密集任务,而 Java/C# 的多线程模型相对不怕?那 Go 又是怎么"两头占便宜"的?

写完后回到第 2、3、5 节对答案,把说错的地方用红色(or 修改)订正。

✅ 总结与自测

今天带走的 4 句话

  1. 后端的第一道必答题:人手有限,请求无限,怎么调度——这叫并发模型,与语言无关。
  2. 三大流派:多线程/线程池(Java/C#)、事件循环(Node.js)、轻量协程(Go),区别在人手成本和代码写法。
  3. Node 的秘密不是"单线程",而是"你的代码单线程 + 慢活外包给 libuv 线程池 + 完成后按铃(回调)"。
  4. 黄金法则:永远别在等待中站着不动;永远别在主线程磨面粉。换成任何语言,这条都成立。

自测清单(能打勾才算今天过关)

  • 能向一个不懂技术的人用面馆比喻讲清 event loop
  • 能说出三种并发模型各自一个优缺点
  • 能解释为什么 setTimeout(fn, 1000) 不保证恰好 1 秒后执行
  • 能把 "async/await" 在 Node 和 C# 里的差别用一句话说清