南京大学 操作系统 (JYY) 学习笔记:并发编程的宏大视角——从超算、Web 到数据中心
写在前面:这是本系列的第十八篇。
理论上说,我们可以用底层的并发编程 API(线程、互斥、同步机制)实现几乎一切实用的并发程序。然而,并发程序“难写”也是出了名的。时至今日,并发 Bugs “若隐若现”的特性依然经常让开发人员抓狂。
如果把多线程的锁全部交给程序员手工维护,这毫无疑问是个过于乐观的假设。因此,编程语言的设计者们在不同的应用场景下,演化出了各自独特的并发/并行编程模型。
本讲内容:一次酣畅淋漓的科普!跳出底层的 C 语言锁机制,来看看在高性能计算 (HPC)、Web 前端和海量数据中心里,大师们是如何优雅地解决并发问题的。

并发编程的核心抽象:计算图
无论是什么领域的并发,其核心抽象都可以被归结为一张计算图 (DAG)。
- 计算发生在节点上,而边则代表着计算结果之间的依赖关系(Happens-before)。
- 最长公共子序列 (LCS) 矩阵的对角线计算。
- 数字电路模拟。
- 深度神经网络的前向/反向传播……
- 更有趣的是,计算图在运行时甚至可能是动态变化的。
如果在底层实现计算图?
- 同步条件: 节点 $ u $ 的所有前置节点 (Predecessors) 都执行完毕。
- 条件变量: 写一个
while循环去等待这个条件成立。 - 信号量: 甚至更麻烦,需要前置节点丢出刚好那么多个“球”。
为什么底层 API 不优雅?
直接用锁和信号量并不是实现计算图的优雅方式。
- 我们会在代码中引入大量与核心业务逻辑无关的干扰代码:
while (...) { cond_wait; }P(); V();mutex_lock();
- 一旦漏写或者写错,就会陷入潜在的数据竞争、死锁、原子性/顺序违反的地狱。
来自编程语言的破局思路 (Approach):
- 增加一些功能受限但极其安全的语法(机制)。
- 本质上这些语法都是在描述“计算图”,但能从编译器层面避免人为失误。
高性能计算 (HPC) 中的并发编程
CRAY-1 超级计算机 (1976)
被誉为 “The world’s most expensive love-seat”(世界上最贵的双人沙发,因为其独特的环形沙发散热设计)。
- 算力:138 MFLOPS @ 115kW
- 对比今天:Apple M4 iPad Pro 是 3.8 TFLOPS。一台薄薄的平板,算力约等于 27,000 台 CRAY-1!

(经典)高性能计算
IBM 定义:“A technology that harnesses the power of supercomputers or computer clusters to solve complex problems requiring massive computation.”
- 源自数值密集型科学计算任务:
- 物理系统模拟(天气预报、航天、能源、制药……)大到宇宙小到量子,有模型就能模拟。
- 矿厂: 简单暴力的哈希碰撞。
- AI: 新时代的高性能计算之王(大模型训练本质上就是无尽的矩阵乘法)。
高性能计算程序:特点
- 物理世界具有“空间局部性”。
- “模拟物理世界”的系统具有
embarrassingly parallel(尴尬的并行 / 完美并行)的特性。各个区块可以独立计算,互不干扰。

高性能计算的解决之道:OpenMP & MPI
因为计算图很容易被静态切分(机器-线程两级任务分解),所以:
MPI:解决跨机器的消息传递。OpenMP:解决单机多核的共享内存并行。
只需要加一行魔法指令:
#pragma omp parallel num_threads(128)
for (int i = 0; i < 1024; i++) {
// 编译器会自动帮你把 1024 次循环拆给 128 个线程去跑!
}
实验通关秘诀:
如果你去偷看llm.c(Karpathy 用 C 写的 GPT-2 训练代码),你会发现整个成千上万行的代码里,和多核并行计算相关的,仅仅只有 5 行极其简单的宏指令!这个机制对非计算机专业的科研人员实在太友好了。翻译成条件变量的话,代码会臃肿上百倍,这就是封装与抽象的力量。
#pragma omp parallel for
#pragma omp parallel for collapse(2)
我们身边的并发编程:Web 的演进
改变世界的 Visual Studio Code (2015)
- 2016 年,JYY 老师做实验的对象还是更“受欢迎”的 Github Atom。
- 当时的 JavaScript 甚至连“保存文件到本地硬盘”的标准库都还没实现利索。
改变世界的 Visual Studio Code (2025)
十年之后,Atom 坟头草都几米高了,VSCode 统治了世界。
- 忽然有一天,发现硬分叉上没法用 C/C++ 了,还以为是代码卡出了 LSP 的 Bug。
- 恍惚间我们好像都忘了:这玩意是个不折不扣的 JavaScript 程序啊!
互联网的开始:Web 1.0
从 PC 时代跨入互联网时代 (1990s)。
- Amazon (1994), Yahoo (1994), eBay (1995), Google (1998)。
- 那是个
<font>,<table>,vbscript和切图工程师一统天下的时代。 - 设计师用 Photoshop 画出精确网页,程序员直接切成表格。响应式设计?不存在的。
<script language="vbscript">
Function validateForm() ' 仅限 Internet Explorer
If document.myForm.username.value = "" Then
MsgBox "请输入姓名!"
validateForm = False
Else
validateForm = True
End If
End Function
</script>
从 Web 1.0 到 Web 2.0:Ajax 与 jQuery 的革命
- Ajax (1999): 允许网页实现“后台异步刷新”。悄悄请求后端,然后局部更新 DOM Tree。网页终于变成了“应用”。
- jQuery (2006): 极其优雅的 DOM 查询和操作语言。2000s 的“土味动画”大爆发。
$(".news_list li a").text("这是假新闻哦,骗你的~"); // jQuery 一把梭
从此,做任何事只要“浏览器”就可以
- 甚至诞生了 ChromeOS。
- 传统 GUI 编程 (GTK, Qt, MFC) 谁用谁痛苦,HTML + CSS 构建 UI 的效率简直降维打击。
Web 2.0 时代的并发挑战:Event-based Concurrency
前端面对的是极其庞杂的网络请求和用户交互,必须并发!但前端工程师能写好锁吗?
- 答案是不能。连底层 C 程序员都写不利索,指望前端写并发?
Data race分分钟教做人。
JavaScript 的终极方案:单线程事件驱动 (Event-based concurrency)
- 禁止任何计算节点并行! JS 自始至终只有一个主线程。
- 允许网络请求、定时器在底层(浏览器 C++ 内核)后台执行。
- 执行完后,把回调函数丢进事件队列 (Event Queue)。主线程空闲时就去拿出来跑。
混沌时代的回调地狱 (Callback Hell)
$.ajax({
url: '/api/user',
success: function(user) {
$.ajax({
url: `/api/user/${user.id}/friends`,
success: function(friends) {
// 嵌套到怀疑人生,缩进变成了金字塔...
}
});
}
});
回归:用 Promise 描述动态计算图
Promise.all([
fetch(...).then(...),
fetch(...).then(...),
]).then(
// 等所有前置依赖都完成了,再执行我!完美契合 DAG 计算图!
).catch(...)
再到后来,直接演化出了最终极的语法糖 async/await,让你用写同步代码的方式写异步计算图。
async function fetchData() {
const response = await fetch('/api/submissions/?token=12345678')
return response.json()
}
年轮“碾过”:寻找好的抽象
- PC $ \rightarrow $ Web $ \rightarrow $ Web 2.0 (UGC) $ \rightarrow $ AI (AGI)
- “框架”是驱动技术发展的原动力。
- 我们需要好的抽象:简单可靠(降低心智负担),灵活通用(能表达复杂的物理世界需求)。
数据中心中的并发编程
互联网 & AI 时代的幕后英雄
- 乌兰察布云谷:全国最低电价 + 全年 10 个月自然风冷,撑起了中国互联网的算力底座。
- 为 AI 提供支持:基础模型的“下半场”其实全在云端数据中心。
The C10K Problem 与高并发危机
Dan Kegel 在 1999 年提出:一台服务器怎么同时处理一万个客户端并发?
如果用传统的“一个请求开一个操作系统的线程”:
- 10K 个请求,即使每个只需 1ms 处理,线程维护和上下文切换的开销也会把机器彻底拖垮,简直就是 Fork Bomb!
数据中心的并发答案 1:Serverless (FaaS)
我不懂并发,我也不管并发!
- 函数即服务 (Function as a Service):系统自动帮你 Scale 到千万级并发。
- 你的代码必须是幂等的 (Idempotence)(执行一次和执行十次结果一样),并且无状态,所有的共享状态全部抛给底层分布式数据库。
数据中心的并发答案 2:Go 与 Goroutine (协程)
小孩子才做选择,多核并行(算力)和单线程异步(轻量)我全都要!
- Goroutine: 概念上像线程,但底层是“操作系统线程 + 用户态协程”的混合体。
- 调度机制:
- Go 在每个物理 CPU 核心上绑定一个真正的 OS 线程。
- 如果一个协程执行了阻塞的 I/O(
sleep,read),Go 的底层 Runtime 会偷偷用非阻塞版本替换掉它。 - 一旦发现要卡住了,Go Runtime 会在用户态极速将其挂起,并立即切换到另一个准备好的 Goroutine。
- 结果: 你完全可以在一台服务器上轻松启动 1,000,000 个 Goroutine 共享处理器算力,而 OS 甚至毫无察觉!
Go 语言的终极哲学:Channel
“Do not communicate by sharing memory; instead, share memory by communicating.”
(不要通过共享内存来通信;相反,要通过通信来共享内存。)
- 共享内存 = 万恶之源(各种死锁和数据竞争)。
- UNIX 时代就有了极优雅的并行机制:管道
cat *.txt | wc -l。 - Go 语言引入了
Channel。通过管道在协程之间扔数据,天然实现了生产者/消费者模型的同步与通信,彻底消灭了底层的加锁烦恼。
总结
Take-away messages:
我们在“更好的编程”这条路上从未停止过努力。
直接操纵 OS 的底层 API(系统调用、互斥锁、条件变量)固然能解决一切问题,但它极其危险且反人类。
针对不同领域的痛点,计算机先驱们发明了“领域特定”的终极解决方案:
- Web 前端: 单线程异步编程 (
Promise/async-await),消灭数据竞争。 - 高性能计算 (HPC):
OpenMP编译指导,让循环计算瞬间吃满多核。 - 数据中心: Go 语言的
Goroutine和Channel,榨干 I/O 密集型服务器的最后一滴性能。
更有趣的是,我们可以看到:改变世界的技术,往往只是当初一个小小的奇思妙想,然后坚持到底得到的。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)