首页 › programming › “并发不是并行”神课深读

“并发不是并行”:一场改变了百万程序员思维方式的演讲

概述:为什么这条视频值得你花30分钟

在编程世界里,很少有哪场演讲能像它一样,只用一个卡通地鼠搬书的比喻,就把困扰无数程序员十几年的概念混淆一次性讲清楚。Rob Pike 的《Concurrency Is Not Parallelism》(并发不是并行)自 2012 年初被上传到 Heroku 官方 YouTube 频道以来,长期是并发主题下被引用最多的演讲之一,播放量达百万级(据频道页面,演讲年代较早,精确数字以当前页面为准),在 Hacker News 和 Reddit 的 r/golang、r/programming 上至今仍被反复翻出讨论。

它值得深读的原因有三个:第一,它纠正的错误几乎人人都在犯——把“并发”当成“并行”的同义词,进而错误地优化程序;第二,它背后连着一条从 1978 年图灵奖得主 C. A. R. Hoare 的 CSP 论文一路延伸到 Go 语言运行时的完整理论谱系,是这个领域少有的“有根”的技术主张;第三,在 AI 大量生成代码的今天,理解“并发是一种程序结构方式,而不是提速手段”这件事,正在变得比以往任何时候都重要——因为 AI 能帮你写出并发形状的代码,却替你判断不了它是不是该并发。

视频基本信息

项目 内容
视频标题 Concurrency Is Not Parallelism(并发不是并行)
演讲者 Rob Pike(贝尔实验室前研究员,UTF-8、Plan 9 参与设计者,Go 语言联合创始人)
场合 Heroku 举办的 Waza 开发者大会
频道 Heroku 官方 YouTube 频道
发布时间 2012 年初(据频道页面)
播放量 百万级(据频道页面,历史数据)
时长 约 26 分钟
核心主张 并发是程序的结构设计问题,并行是程序的执行问题;两者相关但互不包含

先交代一下演讲者的分量。Rob Pike 在贝尔实验室参与了 Unix 系统的后期开发,是 UTF-8 编码方案的共同设计者(与 Ken Thompson),还主导了 Plan 9 操作系统和 Newsqueak、Alef、Limbo 等几门带通道(channel)概念的语言。2007 年起他在 Google 与 Thompson 等人共同设计了 Go 语言。也就是说,这场演讲讲的不是“我对并发的理解”,而是“我为什么把 Go 设计成这样”——演讲者本身就是这条技术路线的立法者。

核心主张拆解

一、两个词,两个世界:并发与并行的严格区分

演讲开篇,Pike 给出了全场最核心的一对定义,这两句话后来被无数幻灯片和教科书引用:

并发(Concurrency):把一个程序组织成多个可独立执行的组成部分(composition of independently executing processes)。 并行(Parallelism):多个计算同时执行(simultaneous execution of possibly related computations)。

注意这两个定义的落点完全不同:并发的关键词是“组织”(composition),说的是你如何设计程序;并行的关键词是“同时执行”,说的是程序如何在硬件上跑起来。并发属于设计层,并行属于执行层。

Pike 强调的关键推论是:并发与并行不是对立关系,也不是包含关系,而是“相关但不同”。一个并发设计的程序,可以在单核机器上以时间片轮转的方式交错运行——它依然是并发的,但从未并行;一个完全不加并发设计的程序,也可以靠多线程暴力堆硬件获得并行。演讲中那句被引用最多的话正是对这种关系的总结:“并发不是比并行更好或更糟,它只是不同。”

为什么这个区分重要?因为现实中的混乱都源于此:当你抱怨“程序慢”时,你面对的是并行问题,解法是多核、向量化、增加吞吐;当你抱怨“代码乱、难维护、回调地狱”时,你面对的是结构问题,解法是更好的程序组织方式。用并发手段去解决性能问题,就像用换个文件柜的方式指望办公室变亮——两件事可能都有用,但因果链完全不同。

二、地鼠搬书:一个比喻讲透“为什么并发是一种设计”

演讲最出名的部分,是 Pike 用几只卡通地鼠搬书的动画做的类比,这个比喻的层次感值得完整还原:

这个比喻对应了并发设计的三个动作:分解(把任务拆成可独立执行的部分)、通信(让各部分交换数据)、同步(让它们在正确的时机协作)。Pike 的主张是:这三个动作做好了,程序的自然形状就是并发的,此时如果硬件给了多核,调度器就能把并发单元铺到多核上获得并行;如果没有多核,你依然赚到了清晰的代码结构。

这里有一个常被忽略的收尾强调:Pike 明确说,地鼠搬书的类比不是在教你怎么优化搬运,而是在说明“并发是更好的组织程序的方式”。观众如果把重点放在“多加地鼠还是多买车”上,就把一场设计演讲读成了性能调优演讲——这是这条视频传播史上最常见的一次误读。

三、理论溯源:从 Hoare 的 CSP 到 Go 的 channel

这场演讲真正硬核的部分,是它把 Go 的并发原语放到了一条长达三十多年的学术谱系上,而不是把它们当成 Go 的发明。

源头是英国计算机科学家 Tony Hoare(C. A. R. Hoare,1980 年图灵奖得主)1978 年发表在《Communications of the ACM》上的论文《Communicating Sequential Processes》(通信顺序进程,简称 CSP)。CSP 的核心思想在当时相当激进:与其让多个执行流通过共享内存、再加锁来协调,不如让它们通过“通信”来同步——两个进程之间立一条通道,发送方发出消息,接收方收下消息,这一收一发本身就是同步点。在无缓冲通道上,发送方会阻塞到接收方就绪,这被称为“会合”(rendezvous)。

CSP 的思想后来被多个工程系统验证:INMOS 公司为 Transputer 芯片设计的 occam 语言几乎逐字实现了 CSP 语义;CSP 的代数方法还发展成了形式化验证工具链,一个著名的应用是 1996 年 Gavin Lowe 用 CSP 模型检验发现了 Needham–Schroeder 认证协议的漏洞并给出修正版。这些都是“通过通信来同步”不是民科方案、而是经过数学和工业双重检验的证据。

Pike 本人则是这条谱系上的连环实践者:贝尔实验室的 Newsqueak(1980 年代末,通道成为一等公民)→ Plan 9 上的 Alef → 贝尔实验室 Inferno 系统的 Limbo → 最终到 Go。Go 把这套思想做了工程化裁剪:goroutine 是极轻量的执行单元(初始栈只有几 KB、按需增长,启动成本远低于操作系统线程),channel 是带类型的通信管道,select 语句允许一个 goroutine 同时等待多个通道事件。演讲中 Pike 用一句话总结了这套设计哲学,它后来刻进了 Go 的官方文档《Effective Go》:

不要通过共享内存来通信,而要通过通信来共享内存。

四、运行时的魔法:为什么“轻量并发”在今天变得可行

地鼠和通道讲的是设计哲学,但这场演讲还有一个不那么显眼、却决定了“channel 这种东西到底实不实用”的技术支点:Go 的运行时调度器。

传统上,在程序里开十万个执行流是不可想象的——操作系统线程的栈以 MB 计,上下文切换动辄微秒级,十万个线程光内存就吃掉几十 GB。Go 的做法是 M:N 调度:把 M 个 goroutine 复用到 N 个操作系统线程上。goroutine 的切换由 Go 运行时在用户态完成,成本低几个数量级;某个 goroutine 阻塞在通道或网络读写上时,运行时会把同一线程上的其他 goroutine 挪走,而不是让操作系统线程干等。2012 年前后 Go 团队(Dmitry Vyukov 主导的调度器重设计)引入了 G-M-P 模型——G 是 goroutine,M 是操作系统线程,P 是逻辑处理器(调度上下文),并配合工作窃取(work stealing)来均衡负载。正是这套调度器让“以通信为主的并发结构”在工程上可以承载十万级并发单元,而不只是课堂上的玩具。

演讲里有个常被当作“彩蛋”的细节值得单独指出:你把 Go 程序设成单核调度(例如 runtime.GOMAXPROCS(1)),程序里成百上千个 goroutine 依然会正确地交错运行、通信、同步,只是慢。这个实验是“并发≠并行”最直接的活体证明——并发结构不依赖多核而存在,多核只是让它跑得更快。

五、划清边界:并发不是性能银弹

整场演讲最容易被曲解、也被 Pike 反复自我澄清的一点,是并发与性能的关系。他明确警告:把程序改成并发结构不等于它会变快。性能提升来自并行执行,而并行执行需要三件事凑齐:硬件有多个执行单元、调度器能把任务铺上去、任务的粒度合适(不会细到通信开销吃掉全部收益)。

这里连着一条更老的定律作为天花板:Amdahl 定律(Gene Amdahl,1967)。程序中无法并行的串行部分会硬性限制加速比——哪怕 95% 的代码都能并行,在无穷多核上最多也只能加速 20 倍。并发设计能帮你把“可并行的部分”干净地暴露出来,但它变不出串行部分所没有的速度。

知识与理论溯源:这张主张有真实的根

把视频里的关键主张逐条对回真实文献与工程史,这张知识图谱是这样的:

视频中的主张 真实来源
通过通信同步、会合式通道 Hoare, Communicating Sequential Processes, CACM 1978;1985 年同名专著发展为进程代数
goroutine/channel 的语言设计 Newsqueak(Pike,1980s末)→ Alef(Plan 9)→ Limbo(Inferno)→ Go
M:N 调度、工作窃取 Go 运行时 GMP 模型(Go 团队,Dmitry Vyukov 的可扩展调度器设计,2012)
通道作为一等公民的语言实践 occam 语言(INMOS,为 Transputer 设计,1980s)
CSP 形式化验证的实际价值 Lowe 1996 用 CSP 模型检验发现并修复 Needham–Schroeder 协议漏洞
串行部分限制加速上限 Amdahl, Validity of the Single Processor Approach, 1967
“通信式”并发的另一条路径 Erlang/OTP 的 Actor 模型(Joe Armstrong,2003 年博士论文系统阐述)

值得注意的是 Erlang 这条对照线:Actor 模型与 CSP 同样主张“不共享、靠消息”,但 Actor 的邮箱是异步的、每个 Actor 有身份地址,而 Go 的无缓冲通道是同步会合的。Pike 在演讲中没有展开这组对比,但理解它之后你会发现 Go 的选择是刻意的:同步通道让“收发即同步”成为语言语义的一部分,程序的正确性论证因此简单得多——代价是某些高吞吐场景要自己加缓冲池。

批判性审查:哪些站得住,哪些要打折

证据充分的部分。 “并发/并行”的概念区分在计算机科学内部没有争议,属于定义层面的澄清。CSP 的理论与工程传承是真实且有据可查的:从 occam 到 Go,从形式化验证到工业部署,这条线经得起最严格的溯源。Go 调度器的工程成绩也有大量生产环境背书——Docker、Kubernetes 这类以并发见长的基础设施软件都是 Go 写的,这至少证明“轻量并发单元 + 通道”在超大规模工程中可用。

需要打折扣或常被误读的部分。

第一,“并发结构让程序更易懂”是一个有条件成立的主张。演讲把它作为提案提出,但没有给出实证研究;四十多年来,并发程序的正确性难题(死锁、竞态、生命周期管理)从未消失,只是换了形状——通道用错了样样会泄漏:一个 goroutine 往无人接收的通道发送,它就永远卡在那里,这就是“goroutine 泄漏”。更诚实的读法是:并发把“哪些代码可能同时跑”这个隐式问题,变成了通道两端显式的数据交接——复杂度被转移和显式化了,而不是被消灭了。

第二,“用通信代替共享内存”不等于“Go 程序不该用锁”。Go 标准库保留了 sync.Mutex、sync.WaitGroup 和原子操作,官方文档的态度一直是“哪个更有表达力用哪个”。实践中大量高性能路径就是锁加共享结构;channel 最适合的场景是任务分发、结果汇聚、事件流,而不是所有数据传递。社区里的绝对化口号(“一切皆 channel”)是对演讲的二次扭曲。

第三,2012 年的演讲有时代局限。彼时 Go 1.0 尚未发布(2012 年 3 月才正式发布),此后运行时变化不小:1.14 引入了基于信号的异步抢占,解决了个别 goroutine 长时间占用调度导致饿死的问题;goroutine 栈管理、调度细节也持续演进。演讲中的思想框架依然成立,但涉及运行时行为的具体描述应以当前文档为准。

第四,公平地说,演讲对“并发设计更容易获得并行”的乐观,在 CPU 世界之外遭遇了竞争方案:Rust 的 async/await 走的是无栈协程加显式状态机路线(借助所有权系统在编译期杜绝数据竞态),C++20、Java 21(虚拟线程)也都给出了自己的轻量并发答案。“轻量执行单元 + 显式数据交接”这个大方向赢了,但具体哪个语言形态胜出,至今仍是进行时。

认知提升与实用启示

第一条:给手头的代码做一次“C/P 测试”。 下次你想写并发代码时,先强制自己回答一个问题:我要解决的是结构问题(回调嵌套、状态散落、事件交织)还是速度问题(单核吃满还嫌慢)?前者用并发的形状去重构,后者先上性能分析器找瓶颈。一个立竿见影的自检实验:把程序设成单核跑一遍(Go 里一行 runtime.GOMAXPROCS(1)),如果结果正确只是变慢,说明你的并发结构是健康的;如果结果直接错了,你写出的不是并发设计,而是裸奔的共享内存。

第二条:用“工作池 + 通道”替换线程/回调的两难。 大量所谓的并发难题,形状其实是同一个:一堆独立任务、有限的执行资源、需要收集结果。对应的通用模式是固定数量的 worker goroutine 从任务通道取活、干完写入结果通道、主流程用 sync.WaitGroup 或关闭通道来收尾。这个模式天然限定了并发度(不会开出十万个连接打爆对端),让“生产者—消费者”的边界在类型系统里可见。用它替换掉嵌套回调或裸线程池,代码的可读性差异立现。

第三条:给数据指定唯一主人。 “通过通信来共享内存”落地成习惯就是一条纪律:每块可变数据在任意时刻只归一个执行流所有,要交接就走通道;确实必须共享的,用锁保护并开启竞态检测器(Go 的 -race 标志,源自 ThreadSanitizer 技术)跑一遍测试。这条纪律同样迁移到其他语言——Rust 的所有权系统把它做成了编译错误,Java 的虚拟线程场景下也依然适用。

视频论证结构图

graph TD
    A[问题:程序为什么乱且慢] --> B{区分两个概念}
    B --> C[并发:程序的结构设计<br/>把任务分解 + 通信 + 同步]
    B --> D[并行:程序的执行状态<br/>需要多核硬件支持]
    C --> E[理论根基<br/>Hoare CSP 1978<br/>通信即同步]
    C --> F[工程载体<br/>goroutine 轻量执行流<br/>channel 类型化通道]
    E --> G[谱系传承<br/>occam / Newsqueak / Limbo]
    F --> H[运行时支撑<br/>M:N 调度 GMP 模型<br/>用户态切换 工作窃取]
    H --> I{硬件有多个核?}
    I -->|有| J[调度器铺开任务<br/>获得并行加速]
    I -->|没有| K[时间片交错运行<br/>结构收益仍在]
    D --> L[天花板<br/>Amdahl 定律<br/>串行部分限制加速比]
    J --> M[结论:并发是更好的组织方式<br/>并行是其副产品]
    K --> M
    L --> M

延伸阅读

  1. Rob Pike: Concurrency Is Not Parallelism(YouTube 检索入口) — 本文解读的演讲本体,Heroku 官方频道发布
  2. C. A. R. Hoare, Communicating Sequential Processes, CACM 1978 — 演讲全部思想的源头论文
  3. Effective Go: Concurrency 一章 — Go 官方对“通信即同步”的工程化阐述

参考来源

  1. Concurrency Is Not Parallelism — Rob Pike(YouTube 检索) — 本文解读的核心视频(Heroku 官方频道,2012 年初上传,播放量据频道页面)
  2. Hoare, Communicating Sequential Processes, CACM 1978 — CSP 理论原始论文
  3. Effective Go — Concurrency — “不要通过共享内存来通信”的官方出处
  4. The Go Blog: Share Memory By Communicating — Go 团队对通道交接模式的官方示例(据 Go 官方博客目录)

本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入