第八章:单线程的堵车与多线程的狂欢(以及地狱)
欢迎回来,旅行者:还记得那个“一心一意”的美好年代吗?
欢迎回来,时间旅行者。在你沉睡之前,你最后操作的可能是那台亲切的 Commodore 641 或是 Apple II2。让我们花点时间,回忆一下那个纯真的年代。
你,你的 BASIC 解释器,还有那块谦逊的 6502 芯片。你们的关系是如此“纯洁”:一对一,忠诚,且具有绝对的排他性。
在你的时代,“程序”就是宇宙的中心。这就是我们今天要讨论的第一个概念,你所熟知的世界:单线程(Single-Threading)。
当你的程序开始运行时,它会完全占有整个 CPU2。从你输入 $POKE 53281, 0 (改变C64的屏幕边框颜色)的那一刻起,到程序执行 $END$,CPU 就是你最忠实的仆人。它有且仅有你这一个主人,它的注意力百分之百地集中在你赋予它的任务上2。
这是一种美妙的、线性的、完全可预测的执行方式3。你的代码从第10行跑到第20行,CPU 绝不会中途开小差,自作主张地跑去执行第500行的代码(除非你用 $GOTO$ 命令它这么做,但那依然是你的意志)。
我们不妨用一个你(沉睡前)也能理解的类比:一个人的小店。
在80年代,编程就像经营一家只有一个员工(CPU)的小店4。这个员工身兼数职:他负责收银(计算),负责上货(从内存读取),还要负责拖地(处理输入/输出)。
这种模式有一个巨大的、无可比拟的优点:零沟通成本。
你(程序员)作为老板,不需要给这个员工开会。员工(CPU)不需要和任何人“同步”工作。他永远不会“误解”你的指令,或者把两份顾客的订单搞混。他是一个“思想纯粹”的模范工人,一次只做一件事,做完再做下一件3。
但这种幸福,也伴随着一个唯一的(且致命的)缺点:堵塞(Blocking)。
当这个唯一的员工跑去仓库找东西时——比如,执行一个 $LOAD "*",8,1,从你那慢得像在冬眠的磁带机上加载数据——整个商店就必须关门歇业。
前台(你的键盘)“冻结”了。顾客(你自己)被完全无视。你只能绝望地盯着那个不断闪烁的光标,祈祷磁带机不要卡带,因为你的“员工”正忙着呢,没空搭理你3。
这就是“单线程的堵车”。它简单,它可预测,但它也……慢得令人发指。
旅行者,在你沉睡的时候,我们这些你的后辈,曾经在很长一段时间里,以为我们靠“堆料”就解决了这个问题。
物理学的背叛:免费午餐的终结与“功率墙”
在你沉睡的头二十年里,我们(你的后辈)简直活在天堂。我们享受着科技史上最长、最美味的“免费午餐”5。
我们甚至不需要写出多好的代码。我们今年写的任何“垃圾”代码,到了明年,都会因为新一代CPU的问世而自动跑得飞快。我们把这个奇迹称为“摩尔定律”。我们以为这种好日子会永远持续下去。
但后来,大概在2004年到2006年之间,我们一头撞在了一堵物理学砌成的叹息之墙上6。
为了让你理解接下来发生的一切——以及你的世界观为何必须被重塑——你必须先理解这场悲剧。
登纳德缩放定律(Dennard Scaling)的终结
在你的时代,以及之后的很长一段时间里,我们都遵循着一个神圣的定律,名叫“登纳德缩放定律”7。这个定律的美妙之处在于:晶体管越小,它消耗的功率和电压也越低7。
这意味着什么?这意味着我们可以在同一块芯片上塞进两倍的晶体管,同时大幅提高它们的运行速度(时钟频率),而不需要担心功耗和散热问题7。这就是“免费午餐”的来源:CPU每年都在(物理上)变得更小、更快、更强,而你的代码一行都不用改。
“功率墙”(The Power Wall)的降临
然而在21世纪初,这个定律失效了。我们把晶体管做得太小了。当晶体管的尺寸逼近纳米级别时,一个幽灵出现了:漏电(Subthreshold Leakage)5。
用一个简单的比喻:想象你的CPU里有几十亿个微型水龙头(晶体管)。在过去,当你“关掉”一个水龙头时,它是能完全关死的。但当我们把水龙头做得太小(比如薄至几个原子的门氧化层),它就开始“滴水”了——就算在“关闭”状态,也会有微小的电流(漏电)穿过去5。
一个水龙头滴水没什么大不了。但几十亿个水龙头一起滴水呢?5 这就汇成了一场无法控制的洪水(巨额功耗)。
而根据物理学定律,所有消耗的电能,最终都会变成一样东西:热8。
时代的悲剧:英特尔的“PresHot”
这场危机的顶点,是一个代号为 “Prescott” 的CPU核心8。
在2004年,英特尔推出了基于 Prescott 核心的 Pentium 4 处理器。他们的计划是宏伟的:他们相信自己可以把时钟频率一路推高到 4 GHz、5 GHz,甚至(在后来的计划中)达到 10 GHz!8
他们失败了。而且败得极其惨烈。
Prescott 核心成了CPU界的“切尔诺贝利”。由于恐怖的漏电问题,它的功耗和发热完全失控。它在“空闲”状态下(什么都不干)的温度就高达50摄氏度(122华氏度);而在全速运行时,它散发的热量是如此巨大,以至于普通的散热器根本压不住它,系统会过热并自动关机8。
愤怒的用户们很快给它起了一个流传千古的外号:“PresHot” (Pres-“热”)8。
英特尔的首席架构师之一,Bob Colwell,后来承认他正是因此而离职,因为他意识到这条追求高频率的道路是一个“技术上的死胡同”(a technologically finished dead end)5。
这就是“功率墙”(The Power Wall)8。我们无法再让一个核心变得更快了,再快它自己就熔化了。
被迫的转向:从“更快”到“更多”
这场“PresHot”灾难,标志着一个时代的结束。CPU制造商们(如英特尔和AMD)被迫承认失败,并调转了整个行业的发展方向8。
既然我们无法再制造出一个更快的核心,那我们唯一的办法,就是在同一块芯片上,塞进多个(相对较慢、也更凉快)的核心7。
这就是“多核处理器”诞生的真正原因。
旅行者,请你务必理解这个关键的转折点:多核处理器的出现,不是工程师们的“锦上添花”,而是物理学的一次“背后捅刀”。
我们从追求“更快”(高频率),被迫转向了追求“更多”(多核心)。
这对你,这位“恐龙级”程序员,意味着什么?
这意味着你那个“一个仆人”的幸福年代,永久地结束了。你(的程序)不再是CPU唯一的“主人”。你现在必须学会的,不再是“指挥”,而是“管理”——管理一群(潜在的)乌合之众。
欢迎来到“管理一个团队”的新时代。
“厨房噩梦”:欢迎来到并发地狱
好了,你现在拿到了一台搭载“酷睿四核”处理器的新电脑。你该如何理解它?
让我们来启用大纲中的核心比喻:“厨房噩梦”(Kitchen Nightmare)9。
你的新CPU(比如一个四核处理器)就像一个拥有四个灶台的专业厨房9。理论上,你可以同时烹饪四道菜,效率是过去的四倍。
但问题来了:这个厨房虽然有四个灶台,但管理极其混乱。这四个灶台(核心)必须共享唯一的刀具、唯一的砧板、唯一的盐罐和唯一的胡椒瓶10。这就是“共享内存”(Shared Memory)11。
而你的程序,现在被分成了四个“厨师”(线程),他们试图同时在这个厨房里工作。
混乱,即将开始。
但在我们描述混乱之前,我们必须(像一个合格的博士研究员那样)先厘清两个让你抓狂的新词汇:进程(Process) 和 线程(Thread)。
- 进程(Process): 在我们的类比中,一个“进程”就是一家独立的餐厅(比如你打开的 “Chrome 浏览器”)。它有自己的完整资源:自己的厨房、自己的菜单、自己的员工、自己的地盘12。
- 线程(Thread): 一个“线程”则是餐厅厨房里的一个厨师12。
关键的区别在于隔离性12:
- 两家不同的餐厅(进程A 和 进程B)互不相干。它们拥有各自独立的厨房(内存空间)。A餐厅的厨师(线程)绝对不可能跑到B餐厅的厨房里(除非操作系统特许),A餐厅着火了(崩溃),B餐厅也照常营业。
- 但是,同一个餐厅里的所有厨师(线程1、2、3)必须共享同一个厨房(同一个进程的内存空间)12。
这就是地狱的根源。在你的80年代,你的程序是“一家只有一个厨师的小店”。而一个现代的程序(比如一个游戏)是“一家有几十个厨师、但只有一个厨房的餐厅”。
为了让你更清晰地理解这种新型的“组织架构”,这里有一份对比表格:
对比:进程(餐厅) vs. 线程(厨师)
| 特性 | 进程 (Process) | 线程 (Thread) |
|---|---|---|
| 类比 | 拥有独立厨房的“餐厅” | 在厨房里干活的“厨师” |
| 定义 | 正在运行的程序的实例12 | 进程内部的执行单元13 |
| 内存空间 | 独立。进程间内存隔离12。 | 共享。同一进程的所有线程共享内存12。 |
| 创建开销 | 高。启动一个新进程就像开一家新餐厅。 | 低。创建一个新线程就像多雇一个厨师12。 |
| 隔离性 | 强。一个进程崩溃通常不影响其他进程12。 | 弱。一个线程的错误可能导致整个进程崩溃12。 |
| 通信 | 复杂。需要操作系统介入(IPC)。 | 简单。直接读写共享内存(但也因此危险)。 |
假装的“同时”:上下文切换(Context Switching)
“等等,” 你可能会问,“如果我只有一个CPU(单核)呢?那不就又回到‘一个厨师’的年代了吗?”
问得好。答案是:不。
因为现代操作系统(OS)是一个“诡计多端”的管理者。就算只有一个CPU(一个灶台),操作系统也会假装它在“并发”执行。
它是怎么做到的?通过一项名为“上下文切换”(Context Switching)的杂耍技艺14。
让我们再次使用类比。这个单核CPU,就像一个患有严重“多动症”(ADHD)的厨师15。
- 他开始炒一道宫保鸡丁(线程A),刚把鸡丁扔下锅……
- 突然,操作系统(厨房经理)拍了一下他的后脑勺:“停!别炒了!马上去切洋葱(线程B)!”
- 厨师(CPU)只好放下炒勺,记住“鸡丁刚下锅,还没放盐”,然后跑去切洋葱15。
- 他刚切了两刀……
- 经理又来了:“停!别切了!马上去和面(线程C)!”
- 厨师(CPU)只好放下菜刀,记住“洋葱切了两刀”,然后跑去和面……
这个过程每秒钟会发生几百甚至几千次。因为切换速度极快,在人类看来,就好像这个厨师(CPU)“同时”在炒菜、切菜、和面15。
这项“杂耍”是有巨大代价的。这个代价就叫“开销”(Overhead)15。
厨师每次“切换”任务,都必须执行一套固定流程:放下旧工具、洗手、擦干、找到新工具、拿出新菜单、回忆上次干到哪了……15。
这个“切换”本身不产生任何价值。它纯属管理开销16。如果厨房经理(操作系统)疯了,让厨师每秒钟切换一万次,那这个可怜的厨师就会把他所有的时间都花在“洗手和回忆菜单”上,而一道菜也做不完。
这种灾难,我们称之为“系统抖动”(Thrashing)15。
“厨房噩梦”催生的新怪物(上):竞争条件 (Race Condition)
好了,旅行者。我们已经搭好了舞台:一个拥挤的厨房(共享内存),一群厨师(多线程)在里面同时开工。
现在,让我们欢迎地狱里的第一批“新怪物”。
Insight 2 (The "Hell" is Shared Memory): 地狱的根源来了。只要你允许多个厨师(线程)同时去碰同一个东西(共享内存),灾难就不可避免。
第一个怪物,名叫“竞争条件”(Race Condition)9。
- 定义: 当两个或多个线程试图同时访问(读/写)同一个共享资源时,最终的结果取决于它们执行的“精确顺序”(即,谁“跑”得快)9。
- 厨房类比: 厨师A 和 厨师B 都想给汤加盐。
- A拿起盐罐(读取“盐量=0”)。
- B也拿起盐罐(读取“盐量=0”)。
- A加了一勺盐(计算“0+1=1”),把盐罐放回去(写入“盐量=1”)。
- B也加了一勺盐(计算“0+1=1”),把盐罐放回去(写入“盐量=1”)。
- 结果: 两个厨师都加了盐,但汤里只有一勺盐。为什么?因为B在A放回盐罐之前就拿起了“旧”的盐罐。
这个Bug听起来很蠢,但在真实世界里,它会导致银行系统凭空印钱。
经典剧本:银行账户的“魔术”17
这是并发教学的“圣杯”案例17。我们必须像分析犯罪现场一样,详细重现它。
- 场景: 你的银行账户(一个共享内存地址)余额为 $500。
- 你(这个魔鬼): 同时在两台ATM机上(线程A 和 线程B),分别取款 $50017。
理想情况(单线程的“小店”)
如果银行系统是你的C64,一次只做一个任务,那么剧本是这样的:
- 机器A (线程A): 开始执行。
- 机器A: 检查余额($500)。够。
- 机器A: 计算新余额 ($500 - $500 = $0)。
- 机器A: 写入新余额 ($0)。
- 机器A: 吐钱 $500。
- 机器B (线程B): 开始执行。
- 机器B: 检查余额($0)。不够。
- 机器B: 拒绝交易。
- 结果: 你取出了 $500,账户余额 $0。银行系统安全。
“竞争条件”下的地狱剧本17
现在,我们来看看在现代多线程“厨房噩梦”中,因为“上下文切换”的时机不对,会发生什么。请看好,魔术要开始了:
| 时间点 | 线程A (ATM 1) | 内存中的余额 | 线程B (ATM 2) | CPU在干嘛? |
|---|---|---|---|---|
| T1 | 开始 | $500 | 运行线程A | |
| T2 | 1. 检查余额。balance = 500。够用。 | $500 | 运行线程A | |
| T3 | (准备计算 500 - 500) | $500 | 上下文切换! (经理拍了A的后脑勺) | |
| T4 | (暂停) | $500 | 开始 | 运行线程B |
| T5 | (暂停) | $500 | 1. 检查余额。balance = 500。够用。 | 运行线程B |
| T6 | (暂停) | $500 | (准备计算 500 - 500) | 上下文切换! (经理又拍了B的后脑勺) |
| T7 | 2. 计算新余额: 500 - 500 = 0 | $500 | (暂停) | 运行线程A |
| T8 | 3. 写入新余额 0 | $0 | (暂停) | 运行线程A |
| T9 | 4. 吐钱 $500 | $0 | (暂停) | 运行线程A (任务完成) |
| T10 | (结束) | $0 | 上下文切换! (B厨师回来了) | |
| T11 | $0 | 2. 计算新余额: 500 - 500 = 0 | 运行线程B | |
| T12 | $0 | 3. 写入新余额 0 | 运行线程B | |
| T13 | $0 | 4. 吐钱 $500 | 运行线程B (任务完成) |
魔术揭晓:
你从两台机器里,各自取走了 $500,总共 $1000。而你的账户余额……最后是 $0。
银行白白损失了 $50017。
这就是“竞争条件”的恐怖之处。它不是一个语法错误。你的代码(balance = balance - amount)看起来完美无缺。但它在千万分之一的概率下,会因为CPU切换的时机(T3和T6)不对,产生灾难性的后果。
这是一个潜伏在代码里的幽灵。
“厨房噩梦”催生的新怪物(下):死锁 (Deadlock)
如果说“竞争条件”是两个厨师动作太快、互相干扰,那么第二个怪物“死锁”(Deadlock)则恰恰相反:它是两个(或多个)厨师互相谦让,直到饿死。
- 定义: 两个或多个线程(厨师)各自占有对方需要的一个资源(工具),并互相等待对方释放,导致所有线程都被永久阻塞,程序“卡死”9。
学术经典:餐饮哲学家问题18
在学术界,我们用一个著名的问题来描述死锁:“餐饮哲学家”(Dining Philosophers Problem)18。
- 场景: 五个哲学家(线程)围着一张圆桌,思考人生。
- 资源: 他们面前有意大利面,但桌上只有五根叉子(资源),每两个哲学家之间放一根。
- 规则: 思考不需要工具。但吃面(访问共享资源)必须同时使用两根叉子(左手和右手的)18。
现在,让我们看看“死锁”剧本是如何上演的:
- 五个哲学家同时结束了思考,他们都饿了。
- 他们同时执行了第一个动作:“拿起我左手边的那根叉子。”18
- 灾难发生。
请想象这个画面:桌上所有的五根叉子,此刻正被五只左手握着。
- 哲学家1:拿着左手的1号叉,等待右手的2号叉。
- 哲学家2:拿着左手的2号叉,等待右手的3号叉。
- 哲学家3:拿着左手的3号叉,等待右手的4号叉。
- 哲学家4:拿着左手的4号叉,等待右手的5号叉。
- 哲学家5:拿着左手的5号叉,等待右手的1号叉。
每个人都占有了一个资源,同时在等待一个“永远不会被释放”的资源(因为他的邻居也正在等待)。没有人会松手。他们将永远保持这个姿势,直到饿死19。
这就是“死锁”。
本章高光:死锁的终极类比——那场“要命的面试”20
“餐饮哲学家”还是太学术了。在程序员的幽默文化中,我们有一个更完美的、堪称“史诗级”的类比来解释“死锁”。它来自一个古老的网络段子,我们必须完整地复现它20:
【剧本开始】
- 场景: 你去一家高科技公司面试。
- 资源: 你需要“Offer”(资源A)。面试官需要“对‘死锁’的解释”(资源B)。
面试官: “你非常优秀。现在,请你解释一下什么是‘死锁’(获取资源B),我们就雇佣你(释放资源A)。”
你: “(微笑)我不能解释。除非你先给我Offer(获取资源A),我才会解释(释放资源B)。”
面试官: “我不能给你Offer(锁住资源A),除非你先解释什么是‘死锁’(等待资源B)。”
你: “(保持微笑)我不能解释(锁住资源B),除非你先给我Offer(等待资源A)。”
面试官: “……”
你: “……”
(两人都卡住了。他们各自“持有并等待”(Hold and Wait)对方的资源,形成了“循环等待”(Circular Wait),并且资源“不可抢占”(No Preemption),面试官不能强行撬开你的嘴。死锁的四大必要条件(18)完美达成。)
(僵持了十分钟后)
面试官(恼羞成怒): “保安!保安(线程C)!把这个人给我赶出去!”
保安(在门外): “抱歉,长官,我进不去(阻塞)。”
面试官: “为什么?!(等待资源‘保安’)”
保安(线程C): “面试还没结束(等待资源‘面试结束’),按照规定,我不能打断面试。”
面试官: “我现在就宣布面试结束了!(试图释放‘面试结束’资源)”
你(线程A): “(冷笑)你不能结束。我还没解释‘死锁’呢(持有资源B,而‘面试结束’依赖于资源B)。”
保安(线程C): “他说的对,长官。他必须先解释‘死锁’(等待资源B),你才能宣布面试结束(释放‘面试结束’资源),然后我才能进去(获取‘面试结束’资源)。”
面试官: “……” (面试官现在在等保安,保安在等你,你又在等面试官)
(全剧终)
这就是“死锁”:一个由“资源”和“等待”构成的、无法解开的逻辑闭环。
人类的反击(1):用“纪律”来驯服怪物
好了,旅行者。我们已经被“竞争条件”搞得账户透支,又被“死锁”困在了面试房间。我们(现代程序员)是如何在这样的地狱里幸存下来的?
Insight 3 (The Three Escape Hatches - Philosophy 1): 第一个解决方案是“纪律”。
既然问题出在“共享”——多个厨师同时去抢盐罐——那我们就制定一个严格的“规矩”:在厨房(共享内存)里,某些关键物品(共享资源)上,必须加一把锁(Lock)。
当一个厨师(线程)要用盐罐时,他必须先“锁”住它。用完之前,谁也别想碰。
为了让你(再次)理解这种“纪律”,我们必须请出并发教学的第二个“圣杯”类比:“加油站的卫生间”21。
互斥锁(Mutex,Mutual Exclusion)
这是最简单、最粗暴,也是最常用的一种锁。
- 类比(21): 想象一个加油站,它的卫生间是单间的,门上只有一把钥匙。
- 工作流程:
- 你(线程A)内急,跑到前台拿走了这把唯一的钥匙(执行
lock())。 - 你进入卫生间,把门锁上(获得“互斥”访问权)。
- 此时,线程B 和 线程C 也来了。他们发现钥匙没了,只能在门口排队(线程被“阻塞”)。
- 你办完事(访问完共享内存),出来,把钥匙还给前台(执行
unlock())21。 - 排在第一位的线程B,拿到钥匙,进去,锁门。
- 你(线程A)内急,跑到前台拿走了这把唯一的钥匙(执行
- 关键特性:所有权(Ownership)22。
- “互斥锁”的铁律是:谁上锁,谁解锁22。
- 你(线程A)拿着钥匙进去的,就必须是你(线程A)出来还钥匙。你不能说“我办完了,让我的朋友线程B帮我还钥匙”,这是绝对禁止的。这就是“所有权”的概念。
信号量(Semaphore)
这是另一种稍微高级点的“锁”,它管理的不是“互斥”,而是“数量”。
为了帮你区分这两个“卫生间管理员”,请看下表:
对比:互斥锁 (Mutex) vs. 信号量 (Semaphore)
| 特性 | 互斥锁 (Mutex) | 信号量 (Semaphore) |
|---|---|---|
| 类比 | 只有1把钥匙的单间卫生间21 | 有N把相同钥匙的N间卫生间21 |
| 目的 | 互斥(Mutual Exclusion)。保护资源,同一时间只许一个线程访问22。 | 同步(Signaling)。控制并发数量,同一时间允许N个线程访问22。 |
| 所有权 | 有。谁 lock,就必须由谁 unlock22。 | 无。任何线程都可以执行 wait 或 signal22。 |
| 类型 | 是一个“锁”(Locking Mechanism)24 | 是一个“信号”或“计数器”(Signaling Mechanism)24 |
“锁”的诅咒
用“锁”来解决问题,听起来很完美,对吗?
错了。
“锁”这种“纪律”,本身就是新地狱的开始。
- 你忘了还钥匙(忘记 unlock): 你(线程A)拿了钥匙进了卫生间,然后(因为代码Bug)在里面睡着了。结果:所有其他在外面排队的线程(B、C、D……)将永远等待下去。整个程序卡死。
- “死锁”的回归: 那个“餐饮哲学家”的问题,就是因为“锁”而产生的。
- 哲学家A:锁了“左手的叉子”,等待“右手的叉子”。
- 哲学家B:锁了“右手的叉子”,等待“左手的叉子”。
- 恭喜,你们又“死锁”了。
事实证明,依靠“纪律”和“规范”来管理共享内存,是极其困难且反人类的。我们花了30年时间,才意识到一件事:
既然“共享”是万恶之源,那我们为什么不干脆……停止共享呢?
终极飞跃:与其“锁门”,不如“分家”
旅行者,这可能是本章最“反直觉”,也是最“现代”的一部分。
我们花了30年时间,试图用各种“锁”(Mutex, Semaphore, Monitor...)来驯服“共享内存”这头怪兽,结果被它折磨得死去活来。
Insight 2 (Revisited): 地狱的根源,是“共享的、可变的状态”(Shared, Mutable State)。
Insight 3 (Philosophies 2 & 3): 真正的“复活”,是采纳两种(或三种)全新的哲学。与其“加锁”,不如“分家”。
方案一(FP的复兴):哲学之“复制” (Replication)
你还记得我们在第七章(大纲)里提到的“函数式编程”(FP)吗?那个60年代的老古董,在2010年代突然“文艺复兴”了9。
为什么?因为“功率墙”5。
“功率墙”把我们逼进了多核地狱,而FP恰好是逃出地狱的“秘密通道”。它的核心思想,就是**“不可变性”(Immutability)**25。
- 回到厨房的比喻: 我们受够了厨师们为了一罐盐(共享内存)打架。
- FP的解决方案: 太简单了。我们不“共享”盐罐了。当一个厨师需要盐时,我们就直接复制一个全新的盐罐递给他25。
- 定义: “不可变性”意味着数据一旦被创建,就永远不能被修改。
- 你不能“改”一个变量的值。
- 你只能“创建”一个包含新值的副本(Copy)。
- 这如何解决并发问题?
这就是为什么FP这个“学术玩具”会突然复兴的唯一原因:在“多核危机”爆发时,它是我们能找到的、唯一一个(在理论上)绝对安全的并发模型。
方案二(Actor模型):哲学之“隔离” (Isolation)
FP的“复制”哲学,在某些情况下(比如超大数据)开销很大。于是,第二种哲学诞生了:彻底的“隔离”。
这就是“Actor 模型”(Actor Model)26。
- 核心思想: 真正的“无共享内存”。
- 类比:“购物车” Actor 军团27。
- 想象一个大型电商网站,有100万个用户正在购物。
- 在Actor模型里,系统会创建 100万个 “购物车 Actor”。
- 工作流程(26):
- 这如何解决并发问题?
方案三(CSP模型):哲学之“有组织的隔离”
这是第三种哲学,也是现代 Go 语言背后的核心思想,称为“通信顺序进程”(Communicating Sequential Processes, CSP)28。
- 核心思想: 它也反对“共享内存”,但它采用了一种更“有组织”的通信方式。
- 圣经格言: “不要通过共享内存来通信;而要通过通信来共享内存。”(Don't communicate by sharing memory; share memory by communicating.)29
- 类比:“星巴克”的奇迹30。
- CSP/Go 的并发模型,就像一个运营极其高效的星巴克咖啡店。
- 角色: 顾客(一个Go程,Goroutine)和 咖啡师(另一个Go程)。
- 关键点: 顾客和咖啡师从不直接对话。他们甚至不需要知道对方的存在。
- 他们之间有两个“传送带”(通道,Channels)30:
- 顾客(线程A)把写好的订单(数据),扔进“点餐通道”(
orders <- "拿铁")。 - 咖啡师(线程B)守在“点餐通道”的另一头,从里面取出订单(
drink := <- orders)。 - 咖啡师做完咖啡,把成品(数据)放到“取餐通道”(
drinks <- "拿铁")。 - 顾客(线程A)守在“取餐通道”的另一头,拿走他的咖啡(
myCoffee := <- drinks)。
- 顾客(线程A)把写好的订单(数据),扔进“点餐通道”(
- 这如何解决并发问题?
- 数据的“所有权”在传递过程中是唯一的。
- 当订单在“顾客”手里时,“咖啡师”碰不到它。当顾客把它扔进“通道”后,他就失去了对它的控制权。当“咖啡师”从通道取出它时,他是那一刻它唯一的“主人”。
- 这种模型,就像田径接力赛里的“接力棒”。数据(接力棒)在同一时间只被一个线程(运动员)持有。
- 这是一种极其优雅的、结构化的并发。你不是在“锁”数据,你是在“传递”数据。
结论:欢迎来到新世界,厨房(终于)干净了
亲爱的时空旅行者,我们这一章的旅程,堪称一场“炼狱”之旅。
我们从你所熟悉的、那个可预测的、但效率低下的“单线程堵车”年代3出发。
我们目睹了“物理学的背叛”——登纳德缩放定律的终结5和“PresHot”的悲剧8,它像一个无情的上帝,把我们所有人都从“单核”的天堂,踹进了“多核”的地狱。
我们一头扎进了“厨房噩梦”9,见识了那群(线程)厨师是如何在“共享内存”这个(唯一的)厨房里制造混乱的。
我们认识了两个全新的、只在噩梦中才有的怪物:
- 竞争条件(Race Condition): 我们观摩了那场精彩的“银行魔术”17,两个线程如何合谋,从$500的账户里取出了$1000。
- 死锁(Deadlock): 我们旁听了那场(永远无法结束的)“要命的面试”20,见证了三个线程(面试官、你、保安)是如何互相等待,直到天荒地老。
我们也看到了人类的反击。
我们先是尝试了“纪律”——我们发明了“卫生间钥匙”(Mutex 和 Semaphore)21,试图用“锁”来规范厨师们的行为。结果发现,“锁”本身就是一种更复杂、更容易出错的新地狱。
最终,在经历了30年的痛苦之后,我们迎来了真正的“飞升”。我们意识到,解决“共享”问题的最好办法,就是**“停止共享”**。
我们走向了(古老的)未来:
旅行者,你并非过时。你只是幸运地跳过了我们(你的后辈)在“锁”的地狱里挣扎的那段“弯路”。
你所熟悉的“隔离”和“线性”的思维,在今天非但没有过时,反而成为了最高级的并发哲学的核心(Actor 和 CSP 内部都是线性的)。你只是需要适应,这个世界上,现在有了一万个“你”,在同时运行。
欢迎来到新世界。厨房很忙,但这一次,我们(终于)有序了。
引用的著作
-
Commodore 64 - Wikipedia, 访问时间为 十月 27, 2025 ↩︎
-
Compiling the Original Commodore 64 KERNAL Source | Hacker ..., 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎
-
Threads Made Simple: Understanding Single vs. Multi-Threading for Beginners - Medium, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎
-
Learn Difference Between Multithreading and Single-Threading - Codefinity, 访问时间为 十月 27, 2025 ↩︎
-
The End of Dennard Scaling - YouTube, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
Lecture 15: Moore's Law and Dennard Scaling, 访问时间为 十月 27, 2025 ↩︎
-
Classic Moore's Law Scaling Challenges Demand New Ways to ..., 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎
-
The Power Wall For CPU Chips - Edward Bosworth, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
multithreading Memes - ProgrammerHumor.io, 访问时间为 十月 27, 2025 ↩︎
-
Shared memory - Wikipedia, 访问时间为 十月 27, 2025 ↩︎
-
Threads vs. Processes: How They Work Within Your Program, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
Processes and Threads | Operating Systems (OS) | Core Computer Science - work@tech, 访问时间为 十月 27, 2025 ↩︎
-
Context switch - Wikipedia, 访问时间为 十月 27, 2025 ↩︎
-
Context Switching Explained: Unveiling Its Hidden Costs | by ..., 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
Understanding Context Switching and Its Impact on System Performance - Netdata, 访问时间为 十月 27, 2025 ↩︎
-
Hacking Banks With Race Conditions | by Vickie Li | The Startup ..., 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
Dining philosophers problem - Wikipedia, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
Dijkstra's dining philosophers problem anyone? : r/ProgrammerHumor - Reddit, 访问时间为 十月 27, 2025 ↩︎
-
Deadlock explained. : r/ProgrammerHumor - Reddit, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎
-
Mutex vs Semaphore Using a Gas Station Bathroom Analogy, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
multithreading - Difference between binary semaphore and mutex ..., 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
Diving Deep With SEMAPHORE And MUTEX | by Ankit Singh - Medium, 访问时间为 十月 27, 2025 ↩︎
-
Mutex vs Semaphore - GeeksforGeeks, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎
-
Functional Programming and Immutable Data Structures in Modern ..., 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
How the Actor Model works by example - TheServerSide, 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎
-
Communicating sequential processes - Wikipedia, 访问时间为 十月 27, 2025 ↩︎
-
The Big Myth of Functional Programming: Immutability Solves All Problems - Blog, 访问时间为 十月 27, 2025 ↩︎
-
Go, Concurrency, and Starbucks. Communicating sequential ..., 访问时间为 十月 27, 2025 ↩︎ ↩︎ ↩︎