写在前面:这是本系列的第十八篇。

理论上说,我们可以用底层的并发编程 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 语言的 GoroutineChannel,榨干 I/O 密集型服务器的最后一滴性能。

更有趣的是,我们可以看到:改变世界的技术,往往只是当初一个小小的奇思妙想,然后坚持到底得到的。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐