背景

至今我们学习的都是 “共享内存多线程” 的并发模型


优点

  • 灵活,能使用硬件提供的所有功能
  • 操作系统内核就是这样构造的

缺点

  • 谁用谁知道
  • 一言难尽

本次课内容与目标

科普一些大家在编程中常见的并发模型

  • 线程和协程
  • 同步和通信

线程和协程

线程

现代编程语言几乎内置都了线程支持

  • JVM 上的语言都继承了 Java 的 monitor (锁 + 条件变量)

// Kotlin code
class SimpleRunnable: Runnable {
  public override fun run() {
    println("${Thread.currentThread()} has run.")
  }
}

Thread(SimpleRunnable()).start()
thread(start = true) {
  println("${Thread.currentThread()} has run.")
}

协程 (Coroutines)

多个可以保存/恢复的执行流

  • 死循环可以随时 “切出”
  • M2 - libco
    • 一个基础款的 “轻量级线程”; “Green Thread”
    • 是构建 (Goroutine, ...) 其他东西的基础

无处不在的协程: Python Generator

def all_integers():
    i = 0
    while True:
        yield (i := i + 1)

for i in filter(lambda x: x % 2, all_integers()):
    print(i)
>>> all_integers
<function all_integers at 0x108bb40d0>

>>> all_integers()
<generator object all_integers at 0x108c8cf90>

  • Python 也支持更灵活的协程 x = yield
    • “Generator object”,但大家也可以按照协程来理解

协程 v.s. 线程

协程:有限的并发和并行

  • 执行流切换只发生在主动放弃 CPU 的时刻
    • yield 的时机 (和行为) 通常是完全确定 (deterministic) 的
    • 相当于插入了很多同步

线程:并发、并行

  • 想同步?自己动手吧

协程 v.s. 线程:都差一点意思

实际系统中会遇到的情况

  • Web 服务器:同时服务海量 I/O 请求
    • 会发生 read() 长时间等待 (例如网络)
      • 协程 → 一人干等,他人围观
      • 线程 → 每个线程都占用可观的操作系统资源
  • 并行计算:同时执行多个计算密集型任务
    • 需要用满多处理器
      • 协程 → 一人出力,他人摸鱼
      • 线程 → 各种并发 bug

Go 和 Goroutine

Go: 小孩子才做选择,多处理器并行和轻量级并发我全都要!

Goroutine: 概念上是线程 (可以并行执行)


实现:每个 CPU 上有一个 Go Worker,自由调度 goroutines

  • 执行 blocking API 时 (例如 sleep, read)
    • Go Worker 偷偷改成 non-blocking 的版本
      • 成功 → 立即继续执行
      • 失败 → 立即 yield 到另一个需要 CPU 的 goroutine
        • 太巧妙了!(另一种实现:把 syscall 移到另一个线程)

例子

同步和通信

Go/Rust/...: Channels (CSP)

Do not communicate by sharing memory; instead, share memory by communicating. ——Effective Go


共享内存 = 万恶之源

  • 在奇怪调度下发生的数据竞争、死锁、……
    • 条件变量:如果不想 broadcast,就要小心 wait 和 signal 之间的 race
    • 信号量:在管理多种资源时就没那么好用了

既然生产者-消费者能解决绝大部分问题,提供一个 API 不就好了?

异步编程

异步编程:单线程前台 + 多线程后台

  • 有一个全局的事件队列,按顺序执行 (run-to-complete)
  • 耗时的 API (Timer, Ajax, ...) 调用会立即返回
    • 条件满足时向队列里增加一个事件
$.ajax( { url: 'http://xxx.yyy.zzz/1',
  success: function(resp) {
    $.ajax( { url: 'http://xxx.yyy.zzz/2',
      success: function(resp) {
        // do something
      },
      error: function(req, status, err) { ... }
    }
  },
  error: function(req, status, err) { ... }
);

异步编程 (cont'd)

好处

  • 并发模型简单了很多
    • 函数之间不存在数据竞争
      • 当然 “事件竞争” 还是不可避免的,例如把判断和扣款放在两个事件里
  • API 依然可以并行
    • 适合网页这种 “大部分时间花在渲染和网络请求” 的场景
      • javascript 代码只负责 “描述” DOM Tree
      • “Web 2.0” 成就了 Google 和 Facebook 帝国

坏处

  • callback hell (祖传屎山)
    • 刚才的代码嵌套 5 层,可维护性已经接近于零了

异步编程:Promise

导致 callback hell 的本质:人类脑袋里想的是 “流程图”,看到的是 “回调”。

The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Promise: 流程图的构造方法 (Mozilla-MDN Docs)

Promise: 描述 Workflow 的 “嵌入式语言”

Chaining

loadScript("/article/promise-chaining/one.js")
  .then( script => loadScript("/article/promise-chaining/two.js") )
  .then( script => loadScript("/article/promise-chaining/three.js") )
  .then( script => {
    // scripts are loaded, we can use functions declared there
  })
  .catch(err => { ... } );

Fork-join

a = new Promise( (resolve, reject) => { resolve('A') } )
b = new Promise( (resolve, reject) => { resolve('B') } )
c = new Promise( (resolve, reject) => { resolve('C') } )
Promise.all([a, b, c]).then( res => { console.log(res) } )

Async-Await: Even Better

async function

  • 总是返回一个 Promise object
  • async_func() - fork

await promise

  • await promise - join

A = async () => await $.ajax('/hello/a')
B = async () => await $.ajax('/hello/b')
C = async () => await $.ajax('/hello/c')
hello = async () => await Promise.all([A(), B(), C()])
hello()
  .then(window.alert)
  .catch(res => { console.log('fetch failed!') } )

总结

总结

本次课内容与目标

  • 了解实际中的编程语言是如何支持并发的
    • 线程和协程 (goroutines)
    • 同步和通信 (channels, Promise)

Take-away messages

  • 编程工具的发展突飞猛进
    • 2000s 以来开源社区出现了非常多的优秀工具
  • 希望每个同学都有一个 “主力现代编程语言”
    • Modern C++, Rust, Javascript, ...