Skip to content

虽然一直在说“锁可以保护临界区资源”,但是忽然发现笔记中没有任何一篇专门说锁的??!

总述

我的理解是锁“隐式”地把多个线程访问资源的顺序给“串行化”了 —— 先看看锁保护资源的整套流程

  1. 多个线程约定访问共享状态前获取同一把锁
  2. 锁通过原子操作保证只有一个线程能获得所有权
  3. 未获得锁的线程不能进入临界区,只能等待
  4. 持锁线程完成一组不可被打断观察的操作
  5. unlock 释放所有权

也就是说,假设一个锁保护了资源,那么各个线程在访问这个资源的时候需要先持有锁(lock),如果一个锁被线程A持有了,那么B就无法持有锁而导致无法访问资源。当A使用完,会归还锁的所有权(瓦达西の心 unlock ),这样其他线程就有机会拥有锁,访问临界区资源 —— 这大抵就是互斥的来源罢

等待锁的方式

假设A已经持有锁了,然后B在等待锁的到来(调用lock失败

那么就延伸出常见的等待策略 ——

自旋

B 不停止运行,而是不断尝试获得锁

C++
while (!TryAcquireLock()) {
    // 继续运行,反复检查
}

所以此时 B 依旧在占用 CPU

我们常说的busy-waiting(忙等待),或者自旋spinning就是这样的一种方式,不断查询直到成功

那么显而易见的优点 —— 如果 A 释放锁的速度很快,B 就可以不用经过睡眠、唤醒等一系列流程直接开始工作;不过反之,如果 A 长时间不释放,则 B 就会一直在浪费 CPU

阻塞

B 如果 lock 失败,不会一直查询,而是进入休眠等待唤醒,那么涉及上下文切换的开销,如果频繁进行上下文切换必然会影响运行效率

混合等待

先原子尝试获取锁,如果失败则先自旋一段时间,如果依旧lock失败,则进入睡眠,等待唤醒

有一小段简单的代码做个演示 ——

C++
void lock() {
    if (TryLock()) {
        return;
    }

    for (int i = 0; i < limited_spin_count; ++i) {
        if (TryLock()) {
            return;
        }
    }

    BlockCurrentThread();
}

Released under the MIT License.