并发控制:互斥(自旋锁、互斥锁和futex)(一)

115 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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);

它们的本质都是:

  1. load
  2. exec (处理器本地寄存器的运算)
  3. 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就是在标记的情况下进行操作。