上次课重新理解了 “操作系统上的程序”
但今天的 CPU 都是多处理器的
如何写程序能利用好多处理器?
学会编写简单的多线程并发程序
正确认识到 “并发编程” 的可怕
Concurrent: existing, happening, or done at the same time.
In computer science, concurrency refers to the ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or in partial order, without affecting the final outcome. (Wikipedia)
为什么在这门课 (先) 讲并发?
malloc
/free
)共享内存的执行流
int x;
void thread_entry() {
printf("Thread %d\n", x++);
}
Thread 1
Thread 2
Thread 0
...
POSIX 为我们提供了线程库 (pthreads)
pthread_create
创建并运行线程pthread_join
等待某个线程结束man 7 pthreads
线程
threads.h
: Simplified Thread APIs操作系统课封装了线程 API (threads.h
)
create(fn)
fn
void fn(int tid) { ... }
tid
是从 1 开始编号join(fn)
fn
#include <threads.h>
void a() { while (1) { printf("a"); } }
void b() { while (1) { printf("b"); } }
int main() {
setbuf(stdout, NULL); // ???
create(a);
create(b);
}
利用 threads.h
就可以写出利用多处理器的程序!
- 代码最终会编译成
指令 - 通过指令访问的内存都是同一个
多线程程序的状态机模型
线程共享代码、数据,拥有独立堆栈
int x;
void thread_func() {
int y; // 通常 &y 不会被传递给其他线程
x++; // 其他线程直接可见
}
多处理器系统中线程的代码可能
x++
,结果会是什么呢?int pay(int t) {
if (balance >= t) {
balance -= t;
return SUCCESS;
} else {
return FAIL;
}
}
并发执行 pay(100)
会发生什么 (balance == 100
)?
balance
是 unsigned
,会发生更严重的后果balance -= t
也非常危险分两个线程,计算 $1+1+1+\ldots+1$ (共计 $2n$ 个 $1$)
#define n 100000000
long sum = 0;
void do_sum() { for (int i = 0; i < n; i++) sum++; }
void print() { printf("sum = %ld\n", sum); }
int main() {
create(do_sum);
create(do_sum);
join(print);
}
结果
单线程程序状态机模型中 “程序一直执行” 的基本假设在多线程环境下不再成立。
原子性:pay()
) 不被干扰
(历史) 1960s,大家争先在共享内存上实现原子性 (互斥)
printf
还能在多线程程序里调用吗?
void thread1() { while (1) { printf("a"); } }
void thread2() { while (1) { printf("b"); } }
我们都知道 printf 是有缓冲区的
buf[pos++] = ch
(pos
共享) 不就 💥 了吗?RTFM!
分两个线程,计算 $1+1+1+\ldots+1$ (共计 $2n$ 个 $1$)
#define n 100000000
long sum = 0;
void do_sum() { for (int i = 0; i < n; i++) sum++; }
void print() { printf("sum = %ld\n", sum); }
int main() {
create(do_sum);
create(do_sum);
join(print);
}
开启编译优化
-O1
: 100000000 (???????)-O2
: 200000000编译器对程序的改写 (涉及内存访问指令的移动或删除) 仅对单线程执行有正确性保证。
编译器保证对任意执行
-O0
) 时一致试一试
for (int i = 0; i < n; i++) sum++;
int x = 0, y = 0;
void thread_1() {
x = 1; printf("y = %d\n", y);
}
void thread_2() {
y = 1; printf("x = %d\n", x);
}
实验结果 (4 x Xeon X7460, 24-cores)
打印的 x | 打印的 y | 概率 |
---|---|---|
0 | 0 | 0.2% (难以接受这个设定啊) |
0 | 1 | 82.3% |
1 | 0 | 17.5% |
1 | 1 | 0.0% |
为了使 CPU 运行得更快,CPU 可以
movl $1, (x) # x = 1, cache miss
# 如果等这条指令执行完,会浪费大量时间
movl (y), %eax # CPU 会立即执行这条指令
# (此时 y = 0)
...
现代处理器 (动态数据流分析):
在现代计算机系统上,即便是一个简单的 x = 1
也会经历:
阻止编译器优化变量读写
void delay() {
for (volatile int i = 0; i < DELAY_COUNT; i++) ;
}
在局部制止编译器调整指令顺序
#define barrier() asm volatile ("":::"memory")
void foo() {
extern int x, y;
x++;
barrier(); // ============================
x++; // 阻止 x 的访问被合并
y++; // y 的访问不能被移到 barrier 之前
}
本学期的重要主题:互斥 (mutual exclusion)
stop_the_world();
... // critical section, 临界区
resume_the_world();
临界区管理
stop_the_world()
之后,整个系统中所有的其他线程都暂停resume_the_world()
后,系统中其他线程才恢复pay()
或者 sum++
了本学期基本不涉及
保证可见性:需要硬件支持
lock xchg
, ...)本次课内容与目标
threads.h
)Take-away message
并发算法是非常有趣 (很难) 的研究问题!
《并发算法与理论》 (秋季学期开课)