互斥锁

互斥锁的工作流程,可以按照如下进行描述:

当一个线程请求一个已经被其他线程持有的互斥锁时,该线程会被阻塞并进入睡眠状态,直到锁被释放。这种方式的优点是不会浪费 CPU 资源,因为线程会被挂起,直到它可以获得锁。此时,其他线程可以取得 CPU 的控制权。然而,线程的挂起和唤醒需要操作系统进行上下文切换(systemcall),这是一个相对昂贵的操作。

一个更为形象的解释:
003-并发控制:互斥-20231010.png

解读
  • 管理员使用自旋锁就表明管理员一直在查看队列中是否有线程;

以下是一个 c 语言的简单实现:

// 定义锁的数据结构
typedef struct __lock_t {
    int flag; // 标志位,用于表示锁是否被持有,0表示未被持有,1表示已被持有
    int guard; // 保护位,用于保护flag字段,防止同时修改flag
    queue_t *q; // 等待队列,用于存储等待获取锁的线程
} lock_t;

// 初始化锁
void lock_init(lock_t *m) {
    m->flag = 0; // 设置锁为未被持有状态
    m->guard = 0; // 设置保护位为0
    queue_init(m->q); // 初始化等待队列
}

// 获取锁
void lock(lock_t *m) {
    while (TestAndSet(&m->guard, 1) == 1) // 自旋,直到获取保护位
        ; 
    if (m->flag == 0) {
        m->flag = 1; // 如果锁未被持有,获取锁
        m->guard = 0; // 释放保护位
    } else {
        queue_add(m->q, gettid()); // 如果锁已被持有,将当前线程添加到等待队列
        m->guard = 0; // 释放保护位
        park(); // 挂起当前线程,等待被唤醒
    }
}

// 释放锁
void unlock(lock_t *m) {
    while (TestAndSet(&m->guard, 1) == 1) // 自旋,直到获取保护位
        ;
    if (queue_empty(m->q))
        m->flag = 0; // 如果等待队列为空,释放锁
    else
        unpark(queue_remove(m->q)); // 如果等待队列非空,唤醒队列中的一个线程,并将锁传递给它
    m->guard = 0; // 释放保护位
}

解读
  • 这个锁实现使用了一个保护位(guard)来保护锁的状态(flag),防止同时修改 flag。当一个线程尝试获取锁时,它首先尝试获取保护位,然后检查锁的状态。
  • 如果锁未被持有,线程获取锁并释放保护位;如果锁已被持有,线程将自己添加到等待队列,然后释放保护位并挂起自己。当一个线程释放锁时,它首先获取保护位,然后检查等待队列。如果等待队列为空,线程释放锁;否则,线程唤醒队列中的一个线程,并将锁传递给它。最后,线程释放保护位;