携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情 实现互斥的根本困难:不能同时读/写共享内存
- load (环顾四周) 的时候不能写,只能 “看一眼就把眼睛闭上”
-
- 看到的东西马上就过时了
- store (改变物理世界状态) 的时候不能读,只能 “闭着眼睛动手”
-
- 也不知道把什么改成了什么
自旋锁(Spin Lock):
改变假设 (软件不够,硬件来凑)
假设硬件能为我们提供一条 “瞬间完成” 的读 + 写指令(load+store)
- 请所有人闭上眼睛,看一眼 (load),然后贴上标签 (store)
-
- 如果多人同时请求,硬件选出一个 “胜者”
- “败者” 要等 “胜者” 完成后才能继续执行
x86 原子操作:LOCK 指令前缀
例子:[sum-atomic.c]
- sum = 200000000
Atomic exchange (load + store)原子交换指令
int xchg(volatile int *addr, int newval) {
int result;
asm volatile ("lock xchg %0, %1"
: "+m"(*addr), "=a"(result) : "1"(newval));
return result; }
用 xchg 实现互斥
如何协调宿舍若干位同学上厕所问题?
- 在厕所门口放一个桌子 (共享变量)
-
- 初始时,桌上是 🔑
实现互斥的协议
- 想上厕所的同学 (一条 xchg 指令)
-
- 天黑请闭眼
- 看一眼桌子上有什么 (🔑 或 🔞)
- 把 🔞 放到桌上 (覆盖之前有的任何东西)//加锁操作
- 天亮请睁眼;看到 🔑 才可以进厕所哦
- 出厕所的同学
-
- 把 🔑 放到桌上//释放锁操作
也就是将别的同学眼睛蒙上,然后自己上厕所,并且加以简单的完成或者请求标识。
这里的exchange算法就是将🔑🔞进行交换
也就是下面的代码
int table = YES; void lock() { retry: int got = xchg(&table, NOPE); if (got == NOPE) goto retry; assert(got == YES); } void unlock() { xchg(&table, YES) }
精简为下面:
int locked = 0; void lock() { while (xchg(&locked, 1)) ; }//得到锁后将locked进行交换 void unlock() { xchg(&locked, 0); }//释放锁的时候再交换为0;
这就是操作系统中的自旋锁。
原子指令保证lock指令按顺序完成.
原子指令,需要保证一致性
Lock 指令的现代实现
在 L1 cache 层保持一致性 (ring/mesh bus)
- 相当于每个 cache line 有分别的锁
- store(x) 进入 L1 缓存即保证对其他处理器可见
-
- 但要小心 store buffer 和乱序执行
L1 cache line 根据状态进行协调
- M (Modified), 脏值
- E (Exclusive), 独占访问
- S (Shared), 只读共享
- I (Invalid), 不拥有 cache line
RISC-V: 另一种原子操作的设计
考虑常见的原子操作:
- atomic test-and-set
-
- reg = load(x); if (reg == XX) { store(x, YY); }
- lock xchg
-
- reg = load(x); store(x, XX);
- lock add
-
- t = load(x); t++; store(x, t);
它们的本质都是:
- load
- exec (处理器本地寄存器的运算)
- store
A对锁进行操作的时候打上标记,在下一次load的时候会是load reserved 就是查看是否有标记,有的话我才进行下一步,如果在A标记完以后,B进行了标记,那A就不能进一步操作了,他会从头开始重新打标记,操作。
LR: 在内存上标记 reserved (盯上你了),中断、其他处理器写入都会导致标记消除
lr.w rd, (rs1) rd = M[rs1] reserve M[rs1]
SC: 如果 “盯上” 未被解除,则写入
sc.w rd, rs2, (rs1) if still reserved: M[rs1] = rs2 rd = 0 else: rd = nonzero
下面是实现:
Compare-and-Swap 的 LR/SC 实现
int cas(int *addr, int cmp_val, int new_val) { int old_val = *addr; if (old_val == cmp_val) { *addr = new_val; return 0; } else { return 1; } }
相当于乐观锁的并发机制,其与自旋锁不同的是,这个不单单记录了值,还打上了标记,用来判断线程进行一系列操作的时候有没有被打扰过,如果被别人动过,那么这一系列操作会重新开始,并重新进行标记,因为LR/SC就是在标记的情况下进行操作。