# 同时做很多事

关于 concurrency 与 async 的速成课。19 个概念——Concurrency 与 Parallelism、Process 与 Thread、Context Switch、Blocking 与 Async/Await、Event Loop、Race Condition、Lock、Deadlock、Message Passing、Immutability、Atomic Operation 等——每个都配有英文名称、一幅画面、一张图和一张带例子与代价的卡片。结尾是一张关联图和一页速查表。核心想法:同时做很多事是软件保持快的方式,而所有 bug 都来自两件事:两份工作在同一时刻碰了同一份数据。

- **Author:** Fedor Chishchin
- **Level:** intermediate
- **Reading time:** ~30 分钟阅读
- **Published:** 2026-06-13
- **Source:** https://fedorthinks.com/zh/learn/concurrency-express

---

## 1. Concurrency 对 parallelism

有两个词常被当成同一个意思来用,其实它们不是。一个讲的是来回兼顾,另一个讲的是手多。把它们分清楚,是后面一切的地基。

### 地图:六组共 19 个概念

> 一张地铁线路图:先看到整条线路,再一站一站地坐过去。

先是**基础**——什么是 concurrency、parallelism 和 CPU core。然后是五组,每章一组:**thread 与 process**、**等待**(blocking、async、event loop)、**共享状态**与 race、**lock** 及其风险,以及从根上消除 race 的**更安全的模式**。

_Diagram: 六组概念——第 1–6 章依次讲它们。第 7 章把一切串起来。_

### 每个概念怎么读

> 一本护照:每个人都是同样的栏目,所以很容易比较。

下面每个概念都用同样的方式讲:它的**英文名称**(记住它——这是你查文档要用的词)、一幅生活中的画面、一张**图**,以及一张带有这些栏目的**卡片**:_问题_、_思路_、_例子_、*代价*和*关联*概念。结尾是一张关联图和一页速查表。

### Concurrency — 来回兼顾多个任务

> 一个厨师照看四道菜——切一会儿,再翻炒,再看看烤箱,在它们之间飞快地切换。

**Concurrency** 是靠在多个任务之间来回切换来应付它们——一个工人在好几件事上都有进展,但绝不会在同一瞬间做两件事。一个厨师同时兼顾四道菜,就是 concurrency。它是一种**组织**工作的方式。

_Diagram: 一个工人,四个任务:它飞快切换,每一个都在往前走——但任何一瞬间只有一个在运行。_

#### Concurrency (靠切换来兼顾)

- **问题:** 任务很多,工人只有一个——而且工人会在等待中浪费时间。
- **思路:** 在任务之间切换,让每一个都有进展。
- **例子:** 一个厨师照看四道菜,轮流各做一步。
- **代价:** 复杂度:任务彼此交错,步骤的顺序变得不可预测。
- **关联:** 常与 **Parallelism** 搭配,但是另一个概念。是 **Async** 和 **Event Loop** 的基础。

### Parallelism — 手多

> 四个厨师,在同一瞬间各做自己的一道菜。

**Parallelism** 是多个任务在多个工人上真正在同一瞬间运行。四个厨师就是 parallelism。它是一种**执行**工作的方式。Concurrency 和 parallelism 常常搭配,但它们是不同的想法:前者讲你怎么安排工作,后者讲有多少只手在干。

_Diagram: 四个 core,四个任务——在同一瞬间,四个都真的在运行。_

#### Parallelism (在同一瞬间运行)

- **问题:** 活儿多到一个工人不够,哪怕它从不等待。
- **思路:** 在多个工人(core)上于同一瞬间运行任务。
- **例子:** 一个大计算被切成几块,用上机器上的每一个 core。
- **代价:** 需要多个 core;它们之间共享的数据会变得危险。
- **关联:** **CPU-bound** 工作的解药。建立在 **CPU Core**、**Thread** 和 **Process** 之上。

### CPU Core — 一个 core 是 concurrent,多个 core 是 parallel

> 一个售票员靠依次处理每个人的几个快步骤,就能服务一整条队;一排售票员则在同一秒里服务好几个人。

处理器的一个 **core** 一次只做一件事,但能在任务之间切换得极快,看起来像在做很多事——这就是单个 core 上的 concurrency。真正的 **parallelism** 需要多个 core 真的同时在跑。现代机器有很多 core,所以真实的软件通常两者兼有:好几个任务在进行(concurrent),其中一些真的在一起运行(parallel)。

_Diagram: 每个 core 内部,任务轮流上(concurrency);两个 core 同时运行(parallelism)。_

#### CPU Core (处理器核心)

- **是什么:** 处理器里的一个执行单元:任何一瞬间它只做一件事。
- **原理:** 在任务之间切换得非常快——所以看起来像在做很多事。
- **例子:** 一台 8 个 core 的机器,真的能在同一瞬间做 8 件事。
- **为什么重要:** core 的数量决定了哪些提速根本有可能。
- **关联:** 一个 core 给出 **Concurrency**,多个 core 给出 **Parallelism**。切换要付出一次 **Context Switch** 的代价。

### 为什么要费这个劲

> 一个服务员如果接完一桌的全部点单、上完菜,然后才去招呼下一桌,就会让满屋子的人白白干等着。

一次只做一件事,会浪费大量的等待时间。一个任务在等的时候——等网络回复、等磁盘、等用户——工人本可以去推进另一个任务。Concurrency 把这段闲置时间夺回来;parallelism 则为重活儿增添纯粹的马力。两者合在一起,就是应用在加载时仍能响应、大计算能用上每一个 core 的原因。代价是这门课其余部分要讲的复杂度。

_Diagram: 一个接一个:工人在等待时闲着。重叠起来:A 在等的时候,B 已经在跑——一切都更早完成。_

> **那个定调的问题:** “我是需要让一个工人别再把时间浪费在等待上(concurrency),还是真的需要更多工人同时运行(parallelism)——我的问题究竟要的是哪一个?”

> Concurrency 是靠切换来兼顾多个任务;parallelism 是在多个 core 上于同一瞬间运行它们。两个不同的想法——一个组织工作,另一个执行工作。

## 2. Thread 与 process

要让事情 concurrent 地运行,你得把工作拆成各自独立的执行线。主要有两种,而它们的区别主要就在一件事上:是否共享内存。

### Process — 隔离的程序

> 两栋分开的楼里有两个分开的厨房——各有各的食材、各有各的台面,不通个电话就碰不到对方的库存。

**Process** 是一个正在运行的程序,拥有自己的**私有内存**,与其他 process 隔离。两个 process 不会不小心弄坏对方的数据,因为它们根本不共享任何数据——要通信,就得有意地互相传消息。这种隔离让 process 安全而稳健,但更重:启动一个花费更多,它们之间说话也更慢。

_Diagram: 每个 process 有自己的内存。它们只通过消息交谈——安全,但更贵。_

#### Process (隔离的程序)

- **问题:** 一个程序可能不小心弄坏另一个程序的数据。
- **思路:** 给每个程序一块自己的、用墙隔开的私有内存。
- **例子:** 机器上运行的两个程序;它们只通过消息交谈。
- **代价:** 更重:启动一个花费更多,process 之间通信也更慢。
- **关联:** **Thread** 住在 process 里面。Process 之间互相交谈就是 **Message Passing**。

### Thread — 与同伴共享内存

> 同一个厨房里好几个厨师,共用同样的台面和食材——协调起来很快,但他们可能在同一瞬间去抓同一把刀。

**Thread** 是 process *内部*的一条执行线,同一个 process 里的 thread **共享同一块内存**。这让它们轻巧、协调起来很快——但也是几乎所有 concurrency bug 的来源,因为两个 thread 可能在同一时间碰同一份数据。共享内存既是力量也是隐患:通信便宜,碰撞危险。

_Diagram: 同一个 process 里的三个 thread,都看着同一块共享内存。_

#### Thread (执行线)

- **问题:** Process 很重,而且同一个程序的各部分需要紧密地共享数据。
- **思路:** 在一个 process 内有多条执行线,共享内存。
- **例子:** 一个厨房里好几个厨师,用同样的台面。
- **代价:** 两个 thread 可能同时碰同一份数据——几乎每一个 race 的根源。
- **关联:** Thread 的共享内存就是 **Shared Mutable State**——于是有了 **Race Condition** 和 **Lock**。

### Context Switch — 在任务之间切换是有代价的

> 一个兼顾着多份活儿的工人,得先放下一份、准确记住自己刚才到哪儿了,再拿起另一份——而这一放一拿全都要花实打实的时间。

当一个 core 从一个任务切换到另一个任务时,它会做一次 **context switch**:保存自己做到哪儿了,再加载下一个任务的状态。它很快,但不是免费的;切得太频繁——成千上万个小任务来回折腾——就会把时间浪费在纯粹的开销上。这就是为什么“多开几个 thread 就行”不会自动变快。

_Diagram: 几次切换不算什么。成千上万个小任务——时间就花在切换本身上了。_

#### Context Switch (任务切换开销)

- **是什么:** core 保存一个任务的状态,再加载另一个任务的状态。
- **例子:** 一个 core 在任务 A 和 B 之间交替,中间有一小段“保存并加载”的停顿。
- **陷阱:** 任务太多时,兼顾本身的花费比它省下的还多(“thrashing”)。
- **要点:** “更多 thread”不会自动等于“更快”。
- **关联:** **Concurrency** 在单个 **CPU Core** 上的代价;随 **Thread** 数量增长。

> **Thread 的问题:** “这些任务需要紧密共享数据(thread,有碰撞风险),还是安全地保持隔离(process,有开销)——我是不是创建得太多,以至于切换成了主要成本?”

> Process 是隔离的,有私有内存,消息传递安全但沉重。Thread 共享内存——轻巧快速,也是大多数 concurrency bug 的根源。

## 3. Blocking 与 async

很多时候,程序并不是在计算——而是在等待。你怎么处理这种等待,是阻塞还是异步,决定了一个工人能一直忙着,还是闲坐着。

### Blocking Call — 停下来干等

> 站在柜台前,店员去后面取你的东西,你什么也不做,直到他回来——你站在那儿的时候,没法帮其他任何人。

**Blocking** 调用是指工人先停下来等结果,然后才做别的事——问一下数据库,就站在那儿等它回答。被阻塞的时候,这个工人什么也完不成。对单个很快的步骤,这没问题;但如果你在慢的东西上阻塞——网络、磁盘、其他服务——你就浪费了本可以用在别的活儿上的那段时间。

_Diagram: 工人发出一个查询,然后闲站着,直到答案回来。_

#### Blocking Call (停下来等)

- **是什么:** 工人停下来等结果的一种调用。
- **例子:** 问数据库——然后站在那儿等它回答。
- **何时没问题:** 单个很快的步骤:几乎没什么可等的。
- **代价:** 遇到慢的东西(网络、磁盘、其他服务),工人就在浪费自己的时间。
- **关联:** **Async / Await** 的反面。

### Async / Await — 一个工人始终忙着

> 在繁忙的熟食店里领一个号:你不用呆站着,而是先去办别的事,等叫到你的号再回来。

**异步**(non-blocking)的做法正相反:你启动一个慢操作,不去等,而是去做别的事,等结果好了再来取。许多语言里的关键字 **async** 和 **await** 表达的正是这个——“开始这件事,让我继续;等它完成再回来找我。”一个工人可以同时有成百上千个慢操作在进行。一个 thread 就是这样服务成千上万个等待中的连接的。

_Diagram: 工人启动 A、B 和 C,去做别的活儿,每个结果好了就来取。_

#### Async / Await (异步,non-blocking)

- **问题:** 阻塞的工人在等慢的东西时闲坐着。
- **思路:** 启动慢的事,不等——继续干;好了再回来。
- **例子:** 一个发出很多网络调用的脚本:用了 async,它们一起跑,而不是一个接一个。
- **代价:** 代码更难读;只在有等待的地方才有帮助。
- **何时使用:** **I/O-bound** 工作:网络、磁盘、其他服务。
- **关联:** 通常跑在 **Event Loop** 上。是 **Blocking Call** 的反面。

### Event Loop — 兼顾所有的等待

> 一位极有条理的前台,接每一通电话,启动那些慢的查询,再处理下一个已经就绪的人——从不被任何一件事卡住。

Async 通常跑在 **event loop** 上:一个工人去启动任务,每当有慢的事完成,就运行等着它的那一小段代码,然后继续往前。感觉像很多事同时在发生,其实是一个工人在就绪的任务之间飞快切换。这就是 **JavaScript** 和 **Node** 这类环境背后的模型——在单个 thread 上兼顾等待,效率高得惊人。

_Diagram: Loop 取下一个就绪的任务,启动慢的工作,然后重复。完成的回复加入队列。_

#### Event Loop (单线程调度器)

- **是什么:** 一个 thread,不断地取下一个就绪的任务,周而复始。
- **原理:** 启动慢的事;它完成时,运行等着它的那一小段代码。
- **例子:** JavaScript 和 Node:在单个 thread 上应付成千上万个等待。
- **陷阱:** 一个繁重的计算会占住这唯一的 thread——其他人都得等。
- **关联:** **Async / Await** 背后的引擎;最适合 **I/O-bound** 工作。

### I/O-bound vs CPU-bound — 你的工作在等什么

> 一份主要在等送货的活儿,需要的是更好的统筹者;一份主要是搬重物的活儿,需要的是更多肌肉,而不是更好的调度。

该用什么工具,取决于你的工作在等什么。**I/O-bound** 工作——主要在等网络、磁盘或其他服务——非常适合 async:一个工人兼顾所有的等待。**CPU-bound** 工作——让一个 core 始终满载的繁重计算——从 async 得不到任何帮助(没有闲置时间可夺回),而需要跨 core 的 **parallelism**。判断错是哪一种,就是某些“提速”毫无作用的原因。

_Diagram: 左:工作主要在等——async 能救它。右:core 始终很忙——你需要更多 core。_

#### I/O-bound vs CPU-bound (等待多 vs 计算多)

- **I/O-bound:** 主要在等网络、磁盘、其他服务 → **async**。
- **CPU-bound:** 繁重计算让 core 忙个不停 → 跨 core 的 **parallelism**。
- **例子:** 很多网络调用——I/O-bound。在每个 core 上做大计算——CPU-bound。
- **陷阱:** 搞混了,“提速”就毫无作用。
- **关联:** 在 **Async / Await** 与 **Parallelism** 之间做选择。

> **Async 的问题:** “这份工作主要在等(I/O-bound——用 async),还是主要在算(CPU-bound——用并行的 core)?用错了,什么也换不来。”

> Blocking 是停下来干等;async 是启动慢的事并保持忙碌。Async 赢在等待(I/O-bound);真正的 parallelism 赢在繁重计算(CPU-bound)。

## 4. 共享状态的难题

这里就是 concurrency 为什么难的核心。只要两样东西能在同一时间碰同一份数据,你就可能遇到一个时有时无、测试里看不见、复现起来让人抓狂的 bug。

### Race Condition — 两只手,一个东西

> 两个人在同一瞬间伸手去拿最后一块饼干——两人都看到它在那儿,两人都去抓,接下来会怎样,取决于那一刹那的精确时机。

**Race condition** 是指结果取决于多个并发操作碰共享数据时的精确时机。两个 thread 都把计数器读成 5,都加一,都写入 6——于是一次计数悄悄丢了,因为正确答案是 7。没有人犯逻辑错误;只是步骤交错得不巧。这是 concurrency 的标志性 bug。

_Diagram: 两个 thread 都读到 5,都写入 6。我们期望 7——一次更新悄悄丢了。_

#### Race Condition (取决于时机的 bug)

- **是什么:** 结果取决于任务在共享数据上的时机。
- **例子:** 两个 thread 都把计数器读成 5,都写入 6——而正确答案是 7。
- **原因:** **共享**且**可变**的数据,加上同时访问。
- **陷阱:** 没有逻辑错误——只是步骤以糟糕的顺序交错了。
- **关联:** 由 **Shared Mutable State** 生出。用 **Lock**、**Message Passing**、**Immutability**、**Atomic Operation** 来治。

### Non-determinism — 可怕之处在于它时有时无

> 汽车里的一阵异响,一拿去给修理工看就消失了——真实、时有时无,而且没法按需展示给人看。

Race condition 之所以讨厌,是因为它们是 **non-deterministic**:取决于时机,所以一千次运行里才出现一次,在你的测试里从不出现,只在生产环境的真实负载下才冒头。同样的输入,可能成功也可能失败,取决于微观层面的调度。这就是 concurrency bug 名声可怕的原因——你没法稳定地复现它们,而“在我机器上是好的”正是你在它出了问题时也会预料到的结果。

_Diagram: 同样的代码,同样的输入:几乎每次运行都通过,只有一次失败——因为时机。_

#### Non-determinism (取决于时机的结果)

- **是什么:** 同样的输入,在不同次运行中给出不同的结果。
- **为什么:** 结果取决于任务在微观层面的调度。
- **例子:** 一个 bug 一千次运行才出现一次——测试里从不出现,只在生产环境的负载下出现。
- **陷阱:** “在我机器上是好的”正是你会预料到的,哪怕它已经坏了。
- **关联:** 让 **Race Condition** 如此危险的原因。这就是为什么让 bug 不可能发生,比去追捕它们更好。

### Shared Mutable State — 真正的危险

> 碰撞只发生在两个人都能抓取并改动的东西上——把共享的柜子锁上,或者给每人各一个,争吵就停了。

注意这个精确的来源:既**共享**(不止一个任务能够到它)又**可变**(它能被改动)的数据。去掉其中任何一个性质,race 就消失了——只有一个任务碰的数据是安全的,没人改动的数据可以放心地随便共享。这是这门课其余部分所依托的关键洞见:每一种修复,其实都是一种控制、限制或避免共享可变状态的方式。

_Diagram: 四个格子里只有一个是危险的:既共享又可变的数据。_

#### Shared Mutable State (共享 + 可改动的数据)

- **是什么:** 多个任务都够得到、并且能被改动的数据。
- **思路:** 去掉“共享”或“可变”——race 就无处栖身。
- **例子:** 两个 thread 都往里写的共享计数器或列表——危险。只归一个任务所有的数据,或不可变的数据——安全。
- **要点:** 这门课里的每一种修复,都是在控制、限制或去除这种状态。
- **关联:** **Race Condition** 的来源。**Lock** 控制它,**Message Passing** 去掉“共享”,**Immutability** 去掉“可变”。

> **Race 的问题:** “这里有哪一份数据,是两个任务可能同时改动的——如果它们的步骤以最糟糕的顺序交错,会发生什么?”

> Race condition 是两个任务同时碰共享的、可变的数据,结果悬在时机上。它时有时无,测试里看不见——是 concurrency 的标志性 bug。

## 5. Lock 与 deadlock

治 race 的经典办法,是确保同一时间只有一个任务碰共享数据。它管用——但用来执行这一点的工具,自己也带来一个出了名的隐患。

### Lock / Mutex — 让一个任务独占访问

> 一间单人卫生间只有一把钥匙:谁拿着钥匙谁进去;其他人都在外面排队等。里面永远不会同时有两个人。

**Lock**(或叫 **mutex**,即“mutual exclusion”互斥)确保同一时间只有一个任务进入某一段代码。碰共享数据之前,任务必须先获取 lock;其他想要的任务要等到它被释放。被保护的那一段叫 **critical section**。用对了,它就把围绕共享数据的混乱抢夺,变成有序的一次一个的队伍,race condition 也就消失了。

_Diagram: Thread A 拿着 lock,在 critical section 里干活。Thread B 在外面等。_

#### Lock / Mutex (互斥)

- **问题:** 两个任务同时改共享数据——一个 race。
- **思路:** 获取 lock → 在 **critical section** 里干活 → 释放。其他人都等着。
- **例子:** 一个 thread 拿到 lock,把计数器加一,然后释放它。
- **代价:** 其他任务要等;有 **Deadlock** 和 **Contention** 的风险。
- **何时使用:** 当共享的可变状态确实无法避免时。
- **关联:** 控制 **Shared Mutable State**。往往更便宜的做法:**Atomic Operation** 或 **Message Passing**。

### Deadlock — 所有人永远在等

> 两个人在一个窄门口,各自都不肯先退一步,非要对方先退——两人都被卡住了,客客气气地,永远如此。

Lock 带来了它自己的经典故障:**deadlock**。任务 A 拿着 lock 1,想要 lock 2;任务 B 拿着 lock 2,想要 lock 1。各自都在等对方放手,而谁也不会放——两个都永远冻住了。它来自以不同顺序获取多个 lock。常见的解法是纪律:总是按同一个约定好的顺序获取 lock,这样 **circular wait**(循环等待)就形不成。

_Diagram: A 拿着 1 想要 2,B 拿着 2 想要 1——圈闭合了,两个都永远等下去。_

#### Deadlock (循环等待)

- **是什么:** 任务们围成一圈互相等待,谁也没法继续。
- **例子:** A 拿着 lock 1 想要 2;B 拿着 2 想要 1。
- **原因:** 以不同顺序获取了多个 lock。
- **解法:** 总是按同一个约定好的顺序拿 lock。
- **关联:** **Lock / Mutex** 的经典隐患。

### Lock Contention — lock 是正确的,但有代价

> 一把卫生间钥匙能维持秩序,但如果人人都不断需要它,就会排起队,整个办公室都慢到那一扇门的速度。

Lock 能修好 race,但有代价:一个任务拿着 lock 的时候,其他任务都得等,所以锁得太多,会把你一开始想从 concurrency 里得到的速度抹掉。过度加锁会造成 **contention**——所有人都在同一个 lock 上排队。所以你要尽可能短地持有 lock,尽可能少地保护东西,并偏好需要更少 lock 的设计。一个正确、但被串行化在一个 lock 上的程序,并不比单 thread 的好多少。

_Diagram: 四个 thread 为一个 lock 排队——工作一次只通过一个。_

#### Lock Contention (在 lock 上排队)

- **是什么:** 很多任务为同一个 lock 排队。
- **例子:** 一个被串行化在一个 lock 上的程序,几乎等于单 thread。
- **代价:** 抹掉 concurrency 带来的收益:正确,但慢。
- **解法:** 持有 lock 的时间要短,保护的东西要尽量少,偏好需要更少 lock 的设计。
- **关联:** **Lock / Mutex** 继 **Deadlock** 之后的第二个代价。

> **Lock 的问题:** “这个 critical section 是不是已经小到不能再小,我是不是以一致的顺序获取 lock 来避免 deadlock——还有,contention 是不是正在悄悄抹掉我从 concurrency 得到的收益?”

> Lock 让一次只有一个任务独占访问,杀掉了 race。但 lock 可能 deadlock,过度加锁会把一切串行化——正确,但慢。

## 6. 更安全的模式

Lock 对付的是共享可变状态的症状。更深一层的做法,是把设计做成要对付的东西更少——而少数几种模式,能让整整一类 concurrency bug 根本不可能出现。

### Message Passing — 别共享内存,传消息

> 不是一群厨师抢着用同一个共享计数器,而是每个人在自己的工位上干活,把做好的盘子沿着出餐口传下去——没有两个人会去够同一样东西。

共享内存之外一个强大的替代方案是 **message passing**:任务不碰同一份数据;它们通过一个**队列**互相发消息。一份数据只归一个任务所有,其他任务通过发消息来请求改动。有一句著名的口号:“不要通过共享内存来通信;要通过通信来共享内存。”去掉共享访问,你就从根上去掉了 race,不需要任何 lock。

_Diagram: 任务把消息发进一个队列。只有拥有者任务碰数据。_

#### Message Passing (用通信代替共享)

- **问题:** 共享内存需要 lock,还会滋生 race。
- **思路:** 一个任务拥有数据;其他任务通过队列发消息来请求改动。
- **例子:** 厨师们在各自的工位上,把盘子沿着出餐口传下去,而不是共用一个计数器。
- **代价:** 消息比直接访问内存慢。
- **何时使用:** 当每份数据都可以只有一个拥有者时。
- **关联:** 去掉 **Shared Mutable State** 里的“共享”。**Process** 之间也是这样交谈的。

### Immutability — 不可变的数据不会被 race

> 给每个人发一份自己的复印件,而不是共用一份原件——大家可以同时阅读,而且没人能在别人正读的东西上乱涂乱画。

如果数据在创建之后永远不变——也就是 **immutable**——那么任意多个任务都可以同时读它,毫无风险,因为危险从来不在于共享,而在于共享*并且改动*。你不是原地改一个值,而是造一个新的。Immutability 去掉了“共享且可变”这个条件的一半,也就去掉了整整一类 bug。这就是函数式风格如此倚重它的原因。

_Diagram: 值 v1 永远不变——任意多个任务都可以读它。一次更新会生成一个新值 v2。_

#### Immutability (不可变的数据)

- **问题:** 有人在别人读共享数据的时候改了它。
- **思路:** 不要原地改一个值——造一个新的。
- **例子:** 每人拿到自己的复印件;没人能在别人的上面乱涂乱画。
- **代价:** 是新的副本,而不是原地编辑。
- **何时使用:** 被很多任务读取的数据。函数式风格的支柱。
- **关联:** 去掉 **Shared Mutable State** 里的“可变”。

### Atomic Operation — 不可分割的更新

> 一个旋转闸机,在每个人通过时以一次不可分割的咔嗒声计数——没有“数到一半”的瞬间能让两个人溜过去。

有些操作是 **atomic** 的:它们作为一个不可分割的步骤发生,不能在半途被打断,所以两个任务不会在更新中途撞见对方。语言和数据库提供了 **atomic counter**、**atomic swap** 和 **transaction**,正是为了那些 race 最爱找上的小型共享更新。当你确实必须共享一个会变的值时,atomic 操作往往是比完整 lock 更便宜、更安全的修复。

_Diagram: “读、加、写”是三步,别的任务可以在中间插进来。Atomic 自增是一步。_

#### Atomic Operation (不可分割的一步)

- **问题:** 一个多步骤的更新可能在半途被打断。
- **思路:** 把更新做成一个不可分割的步骤。
- **例子:** 一个 atomic counter、一次 atomic swap、一个数据库 transaction。
- **代价:** 只适合小型更新,不适合大段逻辑。
- **何时使用:** 一个共享的、会变的值,比如计数器。
- **关联:** 往往比 **Lock / Mutex** 更便宜、更安全;能修复 **Race Condition**。

> **模式的问题:** “我在这里能不能完全避开共享可变状态——靠传消息、靠让数据不可变,或者靠一个 atomic 操作——而不是伸手去拿 lock?”

> 最好的 concurrency bug,是被做成不可能发生的那个:传消息而不是共享,让数据不可变,或者用 atomic 操作——race 就无处栖身。

## 7. 把它用好

Concurrency 是一种你该有意添加的力量,而不是出于条件反射。真正的功夫,是只在问题需要时才去用它,并选择能把活儿干完的最简单的模型。

### 让工具匹配瓶颈

> 对一份全是等待的活儿,你不会多雇搬运工;对一份全是搬重物的活儿,你也不会换个更好的调度员——你让帮助对准时间真正花掉的地方。

在添加 concurrency 之前,先知道你的**瓶颈**。如果工作是 **I/O-bound**——主要在等——单个 thread 上的 async 往往就能简单地解决。如果是 **CPU-bound**——主要在算——你需要跨 core 的真正 **parallelism**。如果两者都不是——又快又简单——那加 concurrency 只会白白买来 bug。最常见的错误,是在普通的顺序代码本来就够快的时候,去伸手拿 thread。

_Diagram: 先问时间花在哪里。等待 → async,计算 → parallelism,都不是 → 保持原样。_

### 让共享的面保持极小

> 一个作坊里,每个人大多独自干活,只有一个小小的、标记清楚的共享工具架——共享的越少,出错的就越少。

既然每个 concurrency bug 都来自共享可变状态,最根本的做法就是**把共享降到最小**。让大部分数据归一个任务所有,能做成不可变的就做成不可变的,把真正共享、会变的部分限制在一个小而被严加看守的面上。共享面极小的设计,是你真正能推理清楚的;一切都共享且可变的设计,就是一座鬼屋。在这里,简单不是可选项——它就是全部的防线。

### 这些概念如何相连

> 一棵家谱树:所有的麻烦有同一个祖先,所有的解药有同一个目标。

这门课里几乎所有东西都围着一个点转。**Thread** 共享内存 → 这就造出了**共享可变状态** → 它滋生出 **race**。四种解药——**lock**、**message passing**、**immutability**、**atomic**——来控制或消除它。Lock 带来了它自己的麻烦:**deadlock** 和 **contention**。另外还有一个按工作负载做的选择:等待 → **async + event loop**,计算 → **parallelism**。

_Diagram: 这门课里各个概念之间的主要联系。_

#### 添加 concurrency 之前

- **瓶颈是什么**——I/O-bound(async)、CPU-bound(parallel),还是都不是(那就别加)?
- **哪些数据是共享且可变的**,两个任务有可能同时碰它吗?
- **我能避免共享吗**——message passing、immutability,还是一个 atomic 操作?
- **如果非用 lock 不可**,critical section 是不是极小,lock 的顺序是不是一致?
- **会不会 deadlock**,我是否已经排除了 circular wait?
- **增加的复杂度值得吗**,还是顺序代码本来就够快了?

#### 说明你用得好的迹象

- 你为等待用 **async**,为计算用 **parallelism**——而不是出于条件反射。
- 你能准确指出哪些数据是**共享且可变**的,而且它们很少。
- 你偏好 **message passing 和 immutability**,而不是到处撒 lock。
- 你的 lock 是**短的、有序的**,而且你已经推理过 **deadlock**。
- 在普通的**顺序**代码本来就够快的地方,你不加 concurrency。

> Concurrency 是有意为之的力量:为等待用 async,为计算用 parallelism,并且有意地看守一个极小的共享面。每个 bug 都追溯到共享可变状态——所以尽量少共享。

## 速查表:全部 19 个概念

### 基础

- **Concurrency** (来回兼顾): 一个工人靠切换来兼顾多个任务。一种组织工作的方式。
- **Parallelism** (手多): 多个任务在多个 core 上于同一瞬间运行。一种执行工作的方式。
- **CPU Core** (处理器核心): 一次只做一件事;一个 core 给出 concurrency,多个 core 给出 parallelism。

### Thread 与 process

- **Process** (隔离的程序): 有自己内存的隔离程序。安全,但重。
- **Thread** (执行线): process 内部的一条执行线;thread 共享内存。快,但有风险。
- **Context Switch** (切换开销): 保存一个任务,加载另一个。不是免费的。

### 等待

- **Blocking Call** (停下来等): 停下来等答案,什么也不做。
- **Async / Await** (non-blocking): 启动慢的事,继续干活,好了再回来。
- **Event Loop** (单线程调度器): 一个 thread 不断地取下一个就绪的任务。
- **I/O-bound vs CPU-bound** (等待 vs 计算): 在等 → async。在算 → parallelism。

### 共享状态

- **Race Condition** (时机 bug): 结果取决于任务在共享数据上的时机。
- **Non-determinism** (时有时无): 同样的输入,不同的结果。这种 bug 很少在测试里出现。
- **Shared Mutable State** (共享 + 可改动): 每个 race 的来源。去掉“共享”或“可变”。

### Lock

- **Lock / Mutex** (互斥): critical section 里一次只有一个任务。
- **Deadlock** (循环等待): 任务围成一圈互相等待。解法:统一的 lock 顺序。
- **Lock Contention** (在 lock 上排队): 在一个 lock 上排队,会抹掉速度。

### 更安全的模式

- **Message Passing** (用通信代替共享): 数据有一个拥有者;其他任务给它发消息。
- **Immutability** (不可变的数据): 数据永不改变——大家可以同时读它。
- **Atomic Operation** (不可分割的一步): 一次不可分割的一步完成的更新。

速成课到此结束 · 7 章 · 19 个概念

接下来是练习:拿一个等得很多的东西——一个一个接一个地发很多网络调用的脚本——把那些调用改成 async,让它们一起跑。看着它在原来零头的时间里就跑完。然后找一个共享的计数器或列表,推理一下如果两个任务在同一时间撞上它会发生什么。当你感受到那份提速、并发现那个 race 的那一刻,这些想法就变具体了。但有一个想法要高过其余:同时做很多事是软件保持快的方式,而每一处难点都追溯到同一个根——两件事在同一时间碰了同一份数据。把那个控制住,你就驯服了 concurrency。
