复习
本次课回答的问题
-
Q: 什么样的任务是需要并行/并发的?它们应该如何实现?
本次课主要内容
- 高性能计算中的并发编程
- 数据中心里的并发编程
- 我们身边的并发编程
一、高性能计算中的并发编程
高性能计算程序:特点
“A technology that harnesses the power of supercomputers or computer clusters to solve complex problems requiring massive computation.” (IBM)
以计算为中心
高性能计算:主要挑战
计算任务如何分解
线程间如何通信
例子:Mandelbrot Set
#include "thread.h"
#include <math.h>
int NT;
#define W 6400
#define H 6400
#define IMG_FILE "mandelbrot.ppm"
static inline int belongs(int x, int y, int t) {
return x / (W / NT) == t;
}
int x[W][H];
int volatile done = 0;
void display(FILE *fp, int step) {
static int rnd = 1;
int w = W / step, h = H / step;
// STFW: Portable Pixel Map
fprintf(fp, "P6\n%d %d 255\n", w, h);
for (int j = 0; j < H; j += step) {
for (int i = 0; i < W; i += step) {
int n = x[i][j];
int r = 255 * pow((n - 80) / 800.0, 3);
int g = 255 * pow((n - 80) / 800.0, 0.7);
int b = 255 * pow((n - 80) / 800.0, 0.5);
fputc(r, fp); fputc(g, fp); fputc(b, fp);
}
}
}
void Tworker(int tid) {
for (int i = 0; i < W; i++)
for (int j = 0; j < H; j++)
if (belongs(i, j, tid - 1)) {
double a = 0, b = 0, c, d;
while ((c = a * a) + (d = b * b) < 4 && x[i][j]++ < 880) {
b = 2 * a * b + j * 1024.0 / H * 8e-9 - 0.645411;
a = c - d + i * 1024.0 / W * 8e-9 + 0.356888;
}
}
done++;
}
void Tdisplay() {
float ms = 0;
while (1) {
FILE *fp = popen("viu -", "w"); assert(fp);
display(fp, W / 256);
pclose(fp);
if (done == NT) break;
usleep(1000000 / 5);
ms += 1000.0 / 5;
}
printf("Approximate render time: %.1lfs\n", ms / 1000);
FILE *fp = fopen(IMG_FILE, "w"); assert(fp);
display(fp, 2);
fclose(fp);
}
int main(int argc, char *argv[]) {
assert(argc == 2);
NT = atoi(argv[1]);
for (int i = 0; i < NT; i++) {
create(Tworker);
}
create(Tdisplay);
join();
return 0;
}
二、数据中心里的并发编程
Google 的数据中心
数据中心程序:特点
“A network of computing and storage resources that enable the delivery of shared applications and data.” (CISCO)
以数据 (存储) 为中心
- 从互联网搜索 (Google)、社交网络 (Facebook/Twitter) 起家
- 支撑各类互联网应用:微信/QQ/支付宝/游戏/网盘/……
算法/系统对 HPC 和数据中心的意义
- 你有 1,000,000 台服务器
- 如果一个算法/实现能快 1%,就能省 10,000 台服务器
- 参考:对面一套房 ≈ 50 台服务器 (不计运维成本)
数据中心:主要挑战
多副本情况下的高可靠、低延迟数据访问
- 在服务海量地理分布请求的前提下
- 数据要保持一致 (Consistency)
- 服务时刻保持可用 (Availability)
- 容忍机器离线 (Partition tolerance)
这门课的问题:如何用好一台计算机?
如何用一台 (可靠的) 计算机尽可能多地服务并行的请求
- 关键指标:QPS, tail latency, …
我们有的工具
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
例子
// Example from "The Go Programming Language"
package main
import (
"fmt"
"time"
)
func main() {
go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(n) // slow
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x < 2 { return x }
return fib(x - 1) + fib(x - 2)
}
go run fib.go
# Fibonacci(45) = 1134903170
现代编程语言上的系统编程
Do not communicate by sharing memory; instead, share memory by communicating. ——Effective Go
共享内存 = 万恶之源
- 在奇怪调度下发生的各种并发 bugs
- 条件变量:broadcast 性能低,不 broadcast 容易错
- 信号量:在管理多种资源时就没那么好用了
既然生产者-消费者能解决绝大部分问题,提供一个 API 不就好了?
package main
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
挑战:程序员多
- 零基础的人你让他整共享内存上的多线程
- 恐怕我们现在用的到处都是 bug 吧???
单线程 + 事件模型
尽可能少但又足够的并发
- 一个线程、全局的事件队列、按序执行 (run-to-complete)
- 耗时的 API (Timer, Ajax, …) 调用会立即返回
$.ajax( { url: 'https://xxx.yyy.zzz/login',
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) { ... }
);
异步事件模型
好处
- 并发模型简单了很多
- 函数的执行是原子的 (不能并行,减少了并发 bug 的可能性)
- API 依然可以并行
- 适合网页这种 “大部分时间花在渲染和网络请求” 的场景
- JavaScript 代码只负责 “描述” DOM Tree
坏处
异步编程: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
jsa = 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
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!') } )
总结
本次课回答的问题
-
Q: 什么样的任务是需要并行/并发的?它们应该如何实现?
Take-away message
- 并发编程的真实应用场景
- 高性能计算 (注重任务分解): 生产者-消费者 (MPI/OpenMP)
- 数据中心 (注重系统调用): 线程-协程 (Goroutine)
- 人机交互 (注重易用性): 事件-流图 (Promise)
- 编程工具的发展突飞猛进
- 自 Web 2.0 以来,开源社区改变了计算机科学的学习方式
- 希望每个同学都有一个 “主力现代编程语言”
- Modern C++, Rust, Javascript, …