第23篇|GIL 与多线程
本篇定位:《Python 教程》专栏二「Python 进阶与工程」第 23 篇。承接第 22 篇把正确性钉死后,正面讲清 GIL(Global Interpreter Lock,全局解释器锁) 与
threading:同一进程里多线程共享什么、GIL 保护什么、何时能加速、何时几乎白忙。为第 24 篇多进程与concurrent.futures、第 25 篇 asyncio、以及 DataLoader / 推理客户端 / FastAPI 里的并发选型打底。读完你将能够:用自己的话解释「GIL ≠ 应用层锁」;判断一段工作是 I/O 密集还是 CPU 密集并选对工具;用
Thread/ThreadPoolExecutor发起与汇合任务;用Lock/Queue处理共享可变状态;在 AI Infra 场景里说出「线程放哪一层、不放哪一层」。预计用时:阅读 35~45 分钟,动手 40~50 分钟。
第 22 篇给了安全带:行为可以被断言。本篇回答下一个工程问题——「我想同时干几件事」时,Python 到底允许你怎么干? 很多人听说「GIL 让多线程没用」,于是要么死磕单线程,要么一上来就上多进程、协程,选型全凭感觉。
一句话定心:GIL 限制的是「同一时刻执行 Python 字节码的线程数」;它不禁止并发,也不替你保护业务数据。 I/O 等待、多数 C/CUDA 算子期间会释放 GIL,线程依然很有用;纯 Python 重计算则几乎享受不到多核——那是第 24 篇与 NumPy/PyTorch 的战场。
建议边读边在练习目录里动手(uv init --no-package,标准库即可,无需额外依赖)。阅读顺序:心智模型 → GIL 是什么 → threading 基础 → 竞态与锁 → 线程池 → AI Infra 选型 → 踩坑。下一篇预告多进程与 concurrent.futures(24):当线程叠不动 CPU 时,进程与统一 futures API 怎么接。
和第 02 篇对照:多线程共享同一进程的对象与引用——这正是「共享可变状态」危险的根源。和第 21 篇对照:竞态很难用肉眼复现,日志与可观测要配合锁顺序。和第 22 篇对照:并发代码更要小步测试;「偶发失败」往往是未保护的共享状态。
动手时建议沿用「小推理客户端」意象:并发打多个 HTTP 端点用线程池;本地拼 prompt / 解析 JSON 仍保持纯函数、少共享;真正的重计算(tokenize、大批量预处理)留给进程或向量化。做完后打开任意成熟库的 concurrent 用法,你会认出同一套形状。
本篇不穷尽所有同步原语(Condition / Semaphore / Barrier)、不深入 CPython GIL 切换实现细节、不替代操作系统课。主线是:心智模型 → GIL 边界 → threading 工作流 → 锁与队列 → 选型与 AI 场景 → 踩坑。和「听说 GIL 就放弃线程」相比,正确价值是:在 I/O 边界叠并发,在共享边界减可变状态。
写并发时保持一个工程直觉:能不共享就不共享;必须共享就串行化或用队列交接。 「加个全局 dict 让各线程往里塞」如果没有锁与生命周期约定,三个月后就会变成传说级偶发 bug。社区里那些「多线程加速纯 Python for 循环」的帖子,往往是误解 GIL 的入口。
也可以把本篇看成专栏二的「并行面」入门:第 15~19 篇把协议与积木立住,第 20~22 篇钉环境 / 眼睛 / 断言,本篇开始谈「同一进程里如何叠任务」。后面多进程、asyncio、服务、Agent,都假设「你知道 GIL 管什么、不管什么」。
读官方 threading 与 concurrent.futures 文档时,按本篇目录对照即可。入门阶段把「会判断密集类型、会起线程池、会锁临界区、会用 Queue」做扎实,比背完整同步原语表更重要。
动手时再记一条:并发的第一原则是正确优先于快——错误的并行比串行更糟,因为它摧毁可复现性。排障时一条带锁的最小复现,往往比一百次「再跑一遍碰巧绿了」更值钱。
和「默认开很多线程碰运气」相比,本篇推荐的默认策略是:I/O 用小线程池;共享用锁或队列;CPU 密集别硬上线程。 这个习惯在 DataLoader、批打 API、夜班告警时都会回报你。
若你刚从第 22 篇「可验证」过来,本篇前两节会换一种体感:同样是工程化,但挂钩点从「验得住」换成「叠得对」。把「我觉得多线程会更快」改成一次对照计时,把「全局计数器」改成 Lock 保护——两条路径走完,GIL 直觉就长出来了。
和「在聊天里口头说并发」相比,可运行示例的价值是:机器可重复、可测、可对比墙钟时间——本地与 CI 上同一套结论。排障时你面对的是计时与竞态复现,而不是「我记得好像快了一点」。
动手前再立一条纪律:先单线程正确,再并行。 先写串行实现并测绿,再引入线程池与锁,最后用计时验证是否真有收益。很多人倒过来「先并行再修竞态」,修的时候容易不自觉扩大临界区——线程变成了复杂度放大器。小步红绿循环在并发里同样成立。
和「线程数等于 CPU 核数」的神话相比,更值钱的是匹配瓶颈:I/O 等的是网络,线程可以多于核;CPU 等的是算力,纯 Python 线程再多也抢同一把 GIL。把这个品味带进 code review,比强制「必须多线程」温和且有效。
一、为什么要谈线程:并发不等于并行
1.1 一张图看清 I/O 与 CPU

图1 何时用线程:I/O 密集 vs CPU 密集
import time
from concurrent.futures import ThreadPoolExecutor
def pretend_io(i: int) -> int:
time.sleep(0.2) # 模拟网络等待;sleep 会释放 GIL
return i * 2
t0 = time.perf_counter()
with ThreadPoolExecutor(max_workers=5) as pool:
results = list(pool.map(pretend_io, range(5)))
print(results, f"{time.perf_counter() - t0:.2f}s")
[0, 2, 4, 6, 8] 0.21s
串行五个 sleep(0.2) 约 1 秒;线程池叠等待后约 0.2 秒量级——这就是「线程有用」的体感来源。
要点:
- 并发(concurrency):结构上能同时推进多项任务(不必真在多核上同时算)。
- 并行(parallelism):同一时刻多核真正同时执行。
- GIL 主要挡的是并行执行 Python 字节码,不挡「等 I/O 时换人」。
1.2 和第 02、21、22 篇的边界
第 02 篇讲引用与共享:多线程看到的是同一批对象。第 21 篇讲可观测:竞态日志交错难读。第 22 篇讲测试:并发失败偶发,更依赖小用例与隔离。本篇补上「同一进程内如何叠任务」的合同。
AI Infra 视角:打多个模型 API、拉多个对象存储文件、等下游工具——典型 I/O;tokenizer 大批量纯 Python 循环——偏 CPU。DataLoader 的
num_workers、推理网关的线程池、Agent 里并行 tool call,选型都先问「等的是网络还是算力」。
1.3 渐进采用
老脚本全是串行 for?可以:先识别循环体是 requests.get 还是重计算;I/O 再包进 ThreadPoolExecutor;共享结果用返回值或 Queue,别先上全局可变 dict。不必一天引入完整 actor 框架,但新 I/O 边界不要再无脑串行硬等。
二、GIL 到底是什么
2.1 一把进程级的「字节码门票」

图2 GIL:同一时刻只有一个线程执行字节码
CPython(你最常用的官方解释器)里,同一进程在任意时刻通常只有一个线程持有 GIL 并执行 Python 字节码。设计动机简化地说:保护解释器内部数据结构(引用计数等),让单线程 Python 代码不必处处加锁。
这意味着:
import dis
def add(a, b):
return a + b
dis.dis(add)
...
LOAD_FAST 0 (a)
LOAD_FAST 1 (b)
BINARY_OP 0 (+)
RETURN_VALUE
字节码一步步跑;GIL 保证「跑这些步」时解释器内部相对安全,但 counter += 1 对你仍是读-改-写,多线程下仍要应用层同步。
2.2 谁会释放 GIL
常见会释放(或在 C 层长时间不算 Python 字节码)的情况:
- 阻塞 I/O:
time.sleep、多数 socket / 文件等待 - 许多 C 扩展的重计算:NumPy ufunc、部分 PyTorch CPU/CUDA 算子
- 显式在扩展里调用相关 API 释放
因此:「纯 Python for 累加」多线程几乎不加速;「等 HTTP」或「NumPy 大矩阵」场景下,线程可能有用。
2.3 自由线程(freethreaded)旁注
较新的 CPython 实验/可选构建在推进无 GIL 或可关闭 GIL 的模式。本系列仍以默认带 GIL 的 CPython 3.12 行为为准写工程建议:你线上大多数镜像、云函数、训练镜像仍是经典模型。听到「GIL 没了」时,先确认你的解释器构建与依赖是否真兼容——不要把实验构建当成今天的默认假设。
三、threading 基础:创建、启动、汇合
3.1 Thread 生命周期

图3 threading:创建 · 启动 · 汇合
import threading
import time
def worker(name: str) -> None:
time.sleep(0.1)
print(f"hello from {name}")
t = threading.Thread(target=worker, args=("A",))
t.start() # 真正启动
t.join() # 等待结束
print("done")
hello from A
done
要点:
- 构造 ≠ 启动:忘了
start()是常见坑。 join():主线程等待子线程;需要「全部做完再往下」时用。daemon=True:主线程结束时可不等 daemon;资源清理不可靠,生产代码优先显式join/ 线程池上下文。
3.2 当前线程与命名
import threading
def show() -> None:
me = threading.current_thread()
print(me.name, me.is_alive())
threading.Thread(target=show, name="worker-1").start()
调试时给线程起名,日志(第 21 篇)里立刻可读。
3.3 为何优先线程池
手建一堆 Thread 容易:忘 join、异常吞掉、无线程数上限。标准库推荐:
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch(i: int) -> str:
return f"item-{i}"
with ThreadPoolExecutor(max_workers=4) as pool:
futures = [pool.submit(fetch, i) for i in range(8)]
for fut in as_completed(futures):
print(fut.result())
with 结束会等待未完成任务(默认),异常在 .result() 时重新抛出——和第 11、22 篇的错误处理习惯一致。第 24 篇会看到 同一套 futures API 换 ProcessPoolExecutor。
四、竞态、锁与队列
4.1 竞态长什么样

图4 竞态与锁:共享可变状态必须串行化
import threading
counter = 0
def bump(n: int) -> None:
global counter
for _ in range(n):
counter += 1 # 非原子:读-改-写
threads = [threading.Thread(target=bump, args=(100_000,)) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # 经常 < 400000
GIL 存在仍可能丢更新:字节码边界上线程可切换,两个线程读到同一旧值再各自写回。
4.2 Lock:临界区
import threading
counter = 0
lock = threading.Lock()
def bump(n: int) -> None:
global counter
for _ in range(n):
with lock:
counter += 1
threads = [threading.Thread(target=bump, args=(100_000,)) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # 400000
400000
推荐 with lock:(与第 17 篇上下文管理器同构),避免漏 release。
原则:
- 锁保护的是不变量,不是「随便包一层就安心」。
- 临界区尽量短:锁内别做慢 I/O。
- 多把锁注意顺序,否则死锁。
4.3 Queue:用交接代替共享写
许多问题不必共享可变计数器——用 queue.Queue 做生产者/消费者:
import queue
import threading
q: queue.Queue[int] = queue.Queue()
def producer() -> None:
for i in range(5):
q.put(i)
q.put(None) # 结束哨兵
def consumer() -> None:
while True:
item = q.get()
if item is None:
break
print("got", item)
threading.Thread(target=producer).start()
c = threading.Thread(target=consumer)
c.start()
c.join()
队列内部已同步;线程间传消息通常比抢着改同一 dict 好测、好讲。
4.4 可重入锁 RLock(点到为止)
同一线程可能再次进入临界区时用 RLock。入门默认 Lock;发现「自己调自己持锁函数」再考虑 RLock,不要无脑升级。
五、对照实验:何时快、何时慢
5.1 I/O:线程池有收益
上一节 sleep 示例已展示。真实世界换成 urllib / httpx 同步客户端打多个 URL,墙钟时间同样常下降(注意限流与连接数)。
5.2 纯 Python CPU:线程池常无收益
import time
from concurrent.futures import ThreadPoolExecutor
def cpu_heavy(n: int) -> int:
s = 0
for i in range(n):
s += i * i
return s
N = 3_000_000
t0 = time.perf_counter()
_ = [cpu_heavy(N) for _ in range(4)]
print("serial", f"{time.perf_counter() - t0:.2f}s")
t0 = time.perf_counter()
with ThreadPoolExecutor(max_workers=4) as pool:
list(pool.map(cpu_heavy, [N] * 4))
print("threads", f"{time.perf_counter() - t0:.2f}s")
在典型 CPython 上,线程版往往不比串行快(甚至略慢,因切换)。这不是你的机器坏了——是 GIL 下的预期。把同一模式换成第 24 篇的 ProcessPoolExecutor,或把循环换成 NumPy,才会看到另一类曲线。
5.3 释放 GIL 的扩展:灰色地带
# 示意:大数组运算在 C 层,常释放 GIL
import numpy as np
from concurrent.futures import ThreadPoolExecutor
def mat_work(_: int) -> float:
a = np.random.rand(800, 800)
return float(a @ a.T).sum()
with ThreadPoolExecutor(max_workers=2) as pool:
print(list(pool.map(mat_work, range(2))))
是否加速取决于算子是否真正并行、是否吃满内存带宽。结论要测量(第 21 篇 profiler),不要背口诀替代计时。
六、AI Infra:线程放哪一层
6.1 选型地图

图5 AI Infra:线程适合放在哪一层
6.2 Agent 与工具调用
Agent 循环里「并行打两个互不依赖的 tool」很适合线程池或 asyncio:共享的是不可变输入与返回结果,而不是可变全局「当前对话状态」被多线程乱写。状态机仍由单线程编排更清晰——和第 22 篇「测编排、不测模型运气」同向。
6.3 和后续篇章的分工
- 本篇:同一进程、GIL、线程、锁、队列。
- 第 24 篇:多进程绕开 GIL、
ProcessPoolExecutor、futures 统一脸。 - 第 25 篇:单线程协程叠海量 I/O。
- 第 26~28 篇:HTTP / FastAPI / Agent——默认别在请求处理里无锁改全局模型对象。
读到这里,不妨停一秒自检:你能不能画出「一进程多线程抢 GIL」?能不能解释为何 counter += 1 仍要 Lock?能不能对「并发打 API」与「纯 Python 累加」分别给出工具建议?两问卡壳,就回到图 1、图 2、图 4 把箭头再走一遍。
和「等线上偶发再加锁」相比,现在付出的代价极低:返回值代替全局、队列代替共享 dict、临界区 with lock。事故夜里你会感谢今天的自己。也可以把本篇练习当成以后 FastAPI / Agent 篇的并发骨架——池大小与队列先约定好,比事后补齐便宜得多。
再强调一次边界:本篇教你在 CPython 默认 GIL 下正确使用线程;无 GIL 构建、分布式任务队列、Kubernetes 多副本是下一层,会在服务与 Infra 实战里逐步露面。不要因为「听说将来 GIL 会弱化」就今天在业务里无锁共享可变状态。
若你带团队做 code review,可以把本篇收成三条合并门槛:新增并发是否先证明瓶颈类型;共享可变是否有锁/队列/不变设计;线程池是否有界且能关闭。门槛不在于「必须用满核」,而在于不许用错误并行换虚假加速。
七、踩坑大全
通用
坑 1:以为 GIL 会保护 list.append 组合业务。 现象:偶发错数据。原因:GIL 不管你的不变量。解决:锁、队列、或改为无共享。
坑 2:用多线程加速纯 Python CPU 循环。 现象:更慢或持平。原因:字节码串行。解决:多进程、向量化、或换算法。
坑 3:创建了 Thread 却忘 start()。 现象:目标函数从不跑。解决:用 ThreadPoolExecutor;或 checklist。
坑 4:主线程不 join 就退出。 现象:任务被掐断、结果丢。解决:join 或 with ThreadPoolExecutor。
坑 5:锁内做慢 I/O。 现象:吞吐崩、看似「死锁」。解决:缩小临界区;I/O 放锁外。
坑 6:多锁顺序不一致。 现象:死锁,进程挂住。解决:全局锁序;或尽量一把锁 / 无锁设计。
坑 7:无线程数上限地 Thread().start()。 现象:内存与 FD 打满。解决:有界线程池。
macOS / Linux / Windows
坑 8:在已有多线程程序里再 fork(Linux)。 现象:死锁或诡异状态。解决:多进程用 spawn(第 24 篇展开);避免「线程后再 fork」。
坑 9:Windows 控制台编码与线程日志交错。 现象:日志难读。解决:统一 logging(第 21 篇)+ 线程名。
坑 10:macOS 默认线程栈 / 打开文件数偏紧。 现象:大量线程失败。解决:降 max_workers;查 ulimit。
与 AI / 服务叠加
坑 11:多线程同时写同一模型对象 / 会话缓存。 现象:偶发错误输出。解决:每请求独立状态;只读权重可共享,可变缓存加锁或分片。
坑 12:用 daemon 线程做必须落盘的收尾。 现象:进程退出丢数据。解决:显式关闭与 join;生命周期挂到应用 shutdown。
八、小结
合上文章后,试着用自己的话说明:GIL 限制什么、为何 I/O 场景线程仍有用、以及为何业务数据仍要锁或队列。讲得清,才算真的会了。动手至少跑通:一次线程池 I/O 对照 → 一次无锁竞态计数 → 一次 with lock 修复 → 一次 Queue 交接。
- 并发 vs 并行:线程擅长叠等待;GIL 限制同进程 Python 字节码并行。
- GIL ≠ 应用锁:解释器内部安全 ≠ 你的 counter/list 不变量自动成立。
- threading / 线程池:
start/join;生产优先ThreadPoolExecutor。 - 同步:
Lock护临界区;Queue用消息代替共享写。 - 测量选型:I/O → 线程;纯 Python CPU → 进程/向量化;扩展算子要计时。
- AI Infra:并行 tool/HTTP 适合线程;别用线程硬扛 CPU;状态编排保持清晰。
把故事连起来:第 02 篇共享引用 → 第 21 篇可观测 → 第 22 篇可验证 → 本篇同进程并发。专栏二工程刀具继续成套:下一站是多进程与 concurrent.futures(24)。
若只能带走一句话:等 I/O 用线程;抢 CPU 别骗自己;共享可变必同步。
也可以用「三个不要」自检:不要把 GIL 当成业务锁;不要对纯 Python 重计算指望线程加速;不要无界地狂建线程。
再留一个自测:写一个会丢更新的无锁计数,再加锁修绿;用线程池叠五个 sleep;解释为何 NumPy 大矩阵可能例外——三问都顺,本篇就站稳了。
本篇技能会立刻复利:读开源先看它如何限并发;PR 里要求说清瓶颈类型;重构争议先测量再加大池。后续多进程、asyncio、FastAPI、Agent,都假设这层已稳。
和专栏一串起来复习也不亏:对象与引用(02)是共享危险的根源;异常(11)要在 .result() 里浮出;本篇是专栏二把「同进程叠任务」说透的一课。
也可以把收获压成检查清单:会画 GIL 图;会 ThreadPoolExecutor;会 Lock/Queue;能区分 I/O 与 CPU;知道 AI 场景线程边界。五项过关,即可进入多进程篇。
若把本篇压成给同事的三十秒讲解:先画「一进程一 GIL」;再强调「I/O 可叠、CPU 别硬上」;最后提「GIL 不管你的 dict」。三张草图比贴一篇博客更管用。等对方能独立把串行 HTTP 循环改成有界线程池,并解释竞态,就可以一起 code review 并发 PR 了。
动手清单再压缩一版:写 pretend_io 线程池;复现无锁计数;with lock 修;Queue 做生产消费。四步做完,比只「看完文章」扎实一个数量级。
九、课后练习
- (基础) 用
ThreadPoolExecutor(max_workers=5)并行执行 5 次time.sleep(0.3),打印墙钟时间,并与串行对比,用三句话解释差异。 - (基础) 创建
Thread(target=...),演示「只构造不start」与正确start+join的行为差别。 - (基础) 复现无锁多线程
counter += 1丢更新;加上threading.Lock后断言最终值正确(可结合第 22 篇 pytest)。 - (理解) 用
queue.Queue实现一生产者一消费者,用哨兵值结束;说明为何比共享 list+手写锁更简单。 - (理解) 对纯 Python 重循环分别跑串行与
ThreadPoolExecutor,记录时间;再查资料预告:换成进程池你预期会怎样(下篇验证)。 - (理解) 写两段临界区:锁内
sleepvs 锁外sleep,观察吞吐差异,总结「临界区要短」。 - (进阶) 模拟「并发打 8 个假 API」(可用
sleep+随机延迟),限制max_workers=3,用as_completed打印完成顺序。 - (进阶 · AI Infra) 设计「Agent 并行两个 tool」的小接口:主线程编排,线程池只跑互不依赖的阻塞调用;写一段话说明哪些状态绝不能被多线程写,为什么。
下一篇预告
下一篇是《第24篇|多进程与 concurrent.futures》——在搞清「线程叠 I/O、GIL 挡字节码并行」之后,解决「CPU 密集如何真用多核、进程间如何交接、futures 如何用同一张脸切换线程池与进程池」。你会看到:进程默认隔离内存,避开许多竞态,但带来序列化与启动成本;concurrent.futures 让你用几乎同一套 submit/map/as_completed 做选型实验。
专栏二后续路线:多进程与 futures → asyncio → HTTP / FastAPI → 最小 Agent。带着本篇的 GIL 边界与锁纪律,写进程池时记得:别在已有多线程里随意 fork;可序列化入参;用测量说话。
若想提前热身:把本篇 CPU 重循环示例保存下来,下篇用 ProcessPoolExecutor 原样对照墙钟时间。做完后第 24 篇谈 pickle 与 spawn 时你会带着具体体感。
也可以把第 15~23 篇对照:生成器管流、装饰器管横切、上下文管资源、类型管形状、标准库给积木、依赖钉环境、日志给眼睛、测试给安全带、本篇给同进程并发——工程九件套的轮廓已经清晰,下一篇把「跨进程并行」说透。
专栏一用十四篇把「对象与协议」立住;专栏二从迭代器(15)走到 GIL(23),下一站是多进程。下一篇见。
若时间充裕,再多做一件事:打开你最近用过的一个开源 Web / 训练项目,找到它的线程池或 num_workers 配置,记三条「它为何用线程/进程、如何限制并发」。这种阅读比空背 GIL 定义更能把选型刻进肌肉记忆。
进入专栏二至此,可验证面、可观测面与并发面开始交汇:会看见问题,会自动验回归,也要会在正确层级叠任务。下一篇开始把进程与 futures 配齐。若某一节读着抽象,回到对应插图把箭头走一遍,再手打竞态复现与线程池——多数卡住的地方,其实是「GIL、应用锁、多核」被混成同一句话。
写线程或读竞态报告时再记一条:错误往往来自共享可变与错误层级(该用进程却用线程、该用队列却用全局 dict)。和「栈很长但根因很浅」相比,先选对并发模型能省很多时间。
也可以把本篇看成专栏二的「并行入门」补课:前面把协议、积木、环境、眼睛与断言收齐,本篇把同进程叠任务收齐。后面多进程篇教你如何在真多核下加速;asyncio 篇教你如何在单线程里叠海量等待;FastAPI 与 Agent 篇则把并发打进真实请求与工具循环。
若阅读时觉得「线程、进程、协程」总搅在一起,回到图 1 与图 5:先定瓶颈类型,再选层。日常编程三者都要懂一点,但职责不同——混谈是多数初学者卡住的根源。
收官前对照练习:同一段「五个网络等待」,分别用串行 for、线程池、(预习)心算进程池成本。你会清楚感受到:线程不是万能加速器,而是把等待重叠起来的工具——也是专栏二工程化与服务 / Agent 对接的入场券之一。
写完练习后,不妨在自己的仓库里开一个小 PR:只把串行 HTTP 循环改成有界 ThreadPoolExecutor,不改业务语义。这种「小步叠并发」比空读文档更能把工作流刻进肌肉记忆,也更适合团队推广。
若你带新人,可以把本篇插图打印成一页速查:左列场景(HTTP / 纯循环 / NumPy / Agent tool),右列策略(线程池 / 进程池 / 向量化 / 编排单线程)。新人写服务时对着贴,两周后就会自然说出「先量瓶颈」与「GIL 不是业务锁」。工具文化是带出来的,不是靠一次分享会喊出来的。
最后把专栏二的位置再钉一次:协议篇(15~18)解决「怎么正确接线」;标准库与依赖篇(19~20)解决「积木与环境」;可观测与可验证篇(21~22)解决「如何被看见与自动验证」;本篇解决「同进程如何正确叠任务」。五者齐备,脚本才既写得快,又叠得敢——下一篇再给它配上多进程与 futures 的实话。
