真实世界里的并发编程(二)

98 阅读1分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情

如何用好一台计算机?

如何用一台 (可靠的) 计算机尽可能多地服务并行的请求

关键指标:QPS, tail latency, ...

我们有的工具

线程 (threads)

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

协程 (coroutines)

多个可以保存/恢复的执行流 (M2 - libco)

比线程更轻量 (完全没有系统调用,也就没有操作系统状态)

数据中心:协程和线程

数据中心

同一时间有数千/数万个请求到达服务器

计算部分 需要利用好多处理器 线程 → 这就是我擅长的 (Mandelbrot Set)

协程 → 一人出力,他人摸鱼 I/O 部分 会在系统调用上 block (例如请求另一个服务或读磁盘) 协程 → 一人干等,他人围观(一个线程里只能运行一次协程) 线程 → 每个线程都占用可观的操作系统资源

Go 和 Goroutine Go: 多处理器并行和轻量级并发 Goroutine: 概念上是线程,实际是线程和协程的混合体 每个 CPU 上有一个 Go Worker,自由调度 goroutines 执行到 blocking API 时 (例如 sleep, read) Go Worker 偷偷改成 non-blocking 的版本 成功 → 立即继续执行

失败 → 立即 yield 到另一个需要 CPU 的 goroutine 太巧妙了!CPU 和操作系统全部用到 100%

现代编程语言上的系统编程

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

共享内存 = 万恶之源 在奇怪调度下发生的各种并发 bugs 条件变量:broadcast 性能低,不 broadcast 容易错 信号量:在管理多种资源时就没那么好用了

既然生产者-消费者能解决绝大部分问题,提供一个 API 不就好了? producer-consumer.go 缓存为 0 的 channel 可以用来同步 (先到者等待)


import "fmt"

var stream = make(chan int, 10)
const n = 4

func produce() {
  for i := 0; ; i++ {
    fmt.Println("produce", i)
    stream <- i
  }
}

func consume() {
  for {
    x := <- stream
    fmt.Println("consume", x)
  }
}

func main() {
  for i := 0; i < n; i++ {
    go produce()
  }
  consume()
}

身边的并发编程

Web 2.0 时代 (1999)

人与人之间联系更加紧密的互联网

“Users were encouraged to provide content, rather than just viewing it.”

你甚至可以找到一些 “Web 3.0”/Metaverse 的线索


是什么成就了今天的 Web 2.0?

浏览器中的并发编程:Ajax (Asynchronous JavaScript + XML)

HTML (DOM Tree) + CSS 代表了你能看见的一切

通过 JavaScript 可以改变它

通过 JavaScript 可以建立连接本地和服务器

人机交互程序:特点和主要挑战

特点:不太复杂

既没有太多计算

DOM Tree 也不至于太大 (大了人也看不过来)

DOM Tree 怎么画浏览器全帮我们搞定了

也没有太多 I/O

就是一些网络请求

单线程 + 事件模型

尽可能少但又足够的并发

一个线程、全局的事件队列、按序执行 (run-to-complete)

耗时的 API (Timer, Ajax, ...) 调用会立即返回

条件满足时向队列里增加一个事件

  success: function(resp) {
    $.ajax( { url: 'https://xxx.yyy.zzz/cart',
      success: function(resp) {
        // do something
      },
      error: function(req, status, err) { ... }
    }
  },
  error: function(req, status, err) { ... }
);

总结 本次课回答的问题 Q: 什么样的任务是需要并行/并发的?它们应该如何实现?

Take-away message

并发编程的真实应用场景

高性能计算 (注重任务分解): 生产者-消费者 (MPI/OpenMP)

数据中心 (注重系统调用): 线程-协程 (Goroutine)

人机交互 (注重易用性): 事件-流图 (Promise)

编程工具的发展突飞猛进

自 Web 2.0 以来,开源社区改变了计算机科学的学习方式

每个人都要有一个 “主力现代编程语言”

Modern C++, Rust, Javascript, ...