Open supergem3000 opened 9 months ago
5. 多处理器编程:从入门到放弃 (jyywiki.cn) 并发 操作系统作为“状态机的管理者”,引入了共享的状态。(如文件等等) 操作系统是最早的并发程序。 def Tprint(name): sys_write(f'{name}')
5. 多处理器编程:从入门到放弃 (jyywiki.cn)
操作系统作为“状态机的管理者”,引入了共享的状态。(如文件等等) 操作系统是最早的并发程序。
def Tprint(name): sys_write(f'{name}')
def main(): for name in 'AB': sys_spawn(Tprint, name)
并发程序执行结果可能不确定。比如这个程序最终打印可能是AB也可能是BA。 思考:每一个进程,有自己的内存空间,也就是有一个栈、堆,如下。
但是进程内的每一个线程,也有自己的栈。这时候栈在哪呢? # 入门到放弃1:原子性 山寨支付宝示例: ```c #include "thread.h" unsigned long balance = 100; void Alipay_withdraw(int amt) { if (balance >= amt) { usleep(1); // Unexpected delays 程序中途被不知道什么打断 balance -= amt; } } void Talipay(int id) { Alipay_withdraw(100); } int main() { create(Talipay); create(Talipay); join(); // 等前面创建的线程都完成 printf("balance = %lu\n", balance); }
两个线程都检查余额足够,结果都扣了钱。导致余额变负数(无符号数变特别大)
#include "thread.h" #define N 100000000 long sum = 0; void Tsum() { for (int i = 0; i < N; i++) { sum++; } } int main() { create(Tsum); create(Tsum); join(); printf("sum = %ld\n", sum); }
甚至这个求和程序,都不能正确得到2N。把sum++改成一个incq汇编指令,还是不对。
“处理器一次执行一条指令”的基本假设在今天的计算机系统上不再成立。(多处理器)
还是上面求和的例子,如果使用O1优化,发现输出结果是100000000,如果是O2优化,发现输出结果是20000000。
R[eax] = sum; R[eax] += N; sum = R[eax]
sum += N
保证执行顺序:
asm volatile("" ::: "memory")
错误的假设:一个CPU执行一条指令到达下一个状态。 处理器一个时钟周期可以同时执行多条指令(IPC:一个时钟周期执行多少条指令)。 电路将连续的指令编译成更小的μops。
此处解释上边的问题。incq指令翻译成三步:load内存、计算、store内存。所以这个指令也是不原子的。
ARM/x86等等架构,处理器内存模型也都不一样。 说了这么多放弃的内容,其实就是想说:写多线程代码时,不要试图去完全搞清楚处理器的各种细节,老老实实用锁。
def main(): for name in 'AB': sys_spawn(Tprint, name)
两个线程都检查余额足够,结果都扣了钱。导致余额变负数(无符号数变特别大)
甚至这个求和程序,都不能正确得到2N。把sum++改成一个incq汇编指令,还是不对。
入门到放弃2:执行顺序
还是上面求和的例子,如果使用O1优化,发现输出结果是100000000,如果是O2优化,发现输出结果是20000000。
R[eax] = sum; R[eax] += N; sum = R[eax]
sum += N
编译器优化必须假设代码是单线程顺序执行,否则没法优化了。保证执行顺序:
asm volatile("" ::: "memory")
,告诉编译器可能会有修改。入门到放弃3:多处理器之间的可见性
错误的假设:一个CPU执行一条指令到达下一个状态。 处理器一个时钟周期可以同时执行多条指令(IPC:一个时钟周期执行多少条指令)。 电路将连续的指令编译成更小的μops。
ARM/x86等等架构,处理器内存模型也都不一样。 说了这么多放弃的内容,其实就是想说:写多线程代码时,不要试图去完全搞清楚处理器的各种细节,老老实实用锁。