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

161 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情 

互斥锁(Mutex Lock)

自旋锁的缺陷

性能问题 (0)

  • 自旋 (共享变量) 会触发处理器间的缓存同步,延迟增加

性能问题 (1)

  • 除了进入临界区的线程,其他处理器上的线程都在

空转

  • 争抢锁的处理器越多,利用率越低

性能问题 (2)

  • 获得自旋锁的线程

可能被操作系统切换出去

    • 操作系统不 “感知” 线程在做什么
    • (但为什么不能呢?)
  • 实现 100% 的资源浪费

有可能有钥匙的线程在睡觉,然后资源就一直在浪费,其他线程只能傻傻等着。

Scalability(伸缩性): 性能的新维度

同一份计算任务,时间 (CPU cycles) 和空间 (mapped memory) 会随处理器数量的增长而变化。

0

  • sum-scalability.c
  • thread-sync.h
    • 严谨的统计很难
      • CPU 动态功耗
      • 系统中的其他进程
      • ……
    • Benchmarking crimes(性能受到多维度的影响,机器的散热等等等

#include "thread.h" #include "thread-sync.h" #define N 10000000 spinlock_t lock = SPIN_INIT(); long n, sum = 0; void Tsum() { for (int i = 0; i < n; i++) { spin_lock(&lock); sum++; spin_unlock(&lock); } } int main(int argc, char *argv[]) { assert(argc == 2); int nthread = atoi(argv[1]); n = N / nthread; for (int i = 0; i < nthread; i++) { create(Tsum); } join(); assert(sum == n * nthread); } /1个线程工作的时候反而时间最短,因为原子指令的建立本身就需要时间,在多处理器上,并且会造成相应的资源浪费。

自旋锁的使用场景

两个约束

  1. 临界区几乎不 “拥堵”(锁的争抢很少见)
  2. 持有自旋锁时禁止执行流切换(禁止线程带着锁跑)

真正使用场景:操作系统内核的并发数据结构 (短临界区)

  • 操作系统可以关闭中断和抢占
    • 保证锁的持有者在很短的时间内可以释放锁
  • (如果是虚拟机呢...😂)
    • PAUSE 指令会触发 VM Exit
  • 但依旧很难做好

实现线程 + 长临界区(有锁的争抢行为)的互斥

作业那么多,与其干等 Online Judge 发布,不如把自己 (CPU) 让给其他作业 (线程) 执行?

“让” 不是 C 语言代码可以做到的 (C 代码只能计算)

  • 把锁的实现放到操作系统里
    • syscall(SYSCALL_lock, &lk);
      • 试图获得 

lk,但如果失败,就切换到其他线程

    • syscall(SYSCALL_unlock, &lk);
      • 释放 

lk,如果有等待锁的线程就唤醒

实现线程 + 长临界区的互斥 (cont'd)

操作系统 = 更衣室管理员

  • 先到的人 (线程)
    • 成功获得手环,进入游泳馆
    • *lk = 🔒,系统调用直接返回
  • 后到的人 (线程)
    • 不能进入游泳馆,排队等待
    • 线程放入等待队列,执行线程切换 (yield)
  • 洗完澡出来的人 (线程)
    • 交还手环给管理员;管理员把手环再交给排队的人
    • 如果等待队列不空,从等待队列中取出一个线程允许执行
    • 如果等待队列为空,

*lk = ✅

  • 管理员 (OS) 使用自旋锁确保自己处理手环的过程是原子的