携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情
互斥锁(Mutex Lock)
自旋锁的缺陷
性能问题 (0)
- 自旋 (共享变量) 会触发处理器间的缓存同步,延迟增加
性能问题 (1)
- 除了进入临界区的线程,其他处理器上的线程都在
空转
- 争抢锁的处理器越多,利用率越低
性能问题 (2)
- 获得自旋锁的线程
可能被操作系统切换出去
-
- 操作系统不 “感知” 线程在做什么
- (但为什么不能呢?)
- 实现 100% 的资源浪费
有可能有钥匙的线程在睡觉,然后资源就一直在浪费,其他线程只能傻傻等着。
Scalability(伸缩性): 性能的新维度
同一份计算任务,时间 (CPU cycles) 和空间 (mapped memory) 会随处理器数量的增长而变化。
#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个线程工作的时候反而时间最短,因为原子指令的建立本身就需要时间,在多处理器上,并且会造成相应的资源浪费。
两个约束
- 临界区几乎不 “拥堵”(锁的争抢很少见)
- 持有自旋锁时禁止执行流切换(禁止线程带着锁跑)
真正使用场景:操作系统内核的并发数据结构 (短临界区)
- 操作系统可以关闭中断和抢占
-
- 保证锁的持有者在很短的时间内可以释放锁
- (如果是虚拟机呢...😂)
-
- PAUSE 指令会触发 VM Exit
- 但依旧很难做好
实现线程 + 长临界区(有锁的争抢行为)的互斥
作业那么多,与其干等 Online Judge 发布,不如把自己 (CPU) 让给其他作业 (线程) 执行?
“让” 不是 C 语言代码可以做到的 (C 代码只能计算)
- 把锁的实现放到操作系统里
-
- syscall(SYSCALL_lock, &lk);
-
- 试图获得
lk,但如果失败,就切换到其他线程
-
- syscall(SYSCALL_unlock, &lk);
-
- 释放
lk,如果有等待锁的线程就唤醒
实现线程 + 长临界区的互斥 (cont'd)
操作系统 = 更衣室管理员
- 先到的人 (线程)
-
- 成功获得手环,进入游泳馆
- *lk = 🔒,系统调用直接返回
- 后到的人 (线程)
-
- 不能进入游泳馆,排队等待
- 线程放入等待队列,执行线程切换 (yield)
- 洗完澡出来的人 (线程)
-
- 交还手环给管理员;管理员把手环再交给排队的人
- 如果等待队列不空,从等待队列中取出一个线程允许执行
- 如果等待队列为空,
*lk = ✅
- 管理员 (OS) 使用自旋锁确保自己处理手环的过程是原子的