这篇文章通过同自旋锁对比的形式讲解睡眠锁的本质。
在多线程编程中,当一个线程试图获取一个已经被其他线程持有的锁时,它有很多选择,例如自旋等待,即不断地检查锁是否已经被释放;或者睡眠等待,即让出CPU,进入睡眠状态,等待被唤醒。
PAUSE 指令
在x86架构的CPU中,pause
指令会让当前线程暂停一段时间,然后再继续执行。这个暂停的时间通常非常短,只有几个CPU周期。这个指令通常用于自旋锁的实现,以减少CPU的使用率。
然而,pause
指令并不会导致CPU切换到其他线程。当一个线程执行pause
指令时,它仍然占用着CPU,只是在这段时间内,它不会执行任何其他操作。这就是为什么pause
指令可以减少CPU的使用率:它让CPU有机会在短暂的时间内停止执行指令,从而减少了CPU的功耗。
pause
指令的作用是暂停流水线并减少功耗,但它并不会让出CPU给其他线程。也就是说,执行 pause
指令的线程仍然处于运行状态,而不是就绪或等待状态。因此, pause
指令并不能让出CPU,它只是让CPU在等待时消耗更少的资源。
所以,当你在代码中看到pause
指令时,你应该理解为这是一种优化手段,用于减少自旋锁在忙等状态下的CPU使用率,而不是一种让出CPU的方式。因此,即使在自旋等待中插入了pause
操作,也不能改变自旋等待在等待获取锁的过程中会占用CPU的事实。
使用场景
自旋锁和睡眠锁是两种常见的锁机制,它们在不同的场景下有各自的优势。
自旋锁(Spinlock):当一个线程尝试获取自旋锁而锁已经被其他线程持有时,该线程将在一个循环中不断地检查锁是否可用。由于该线程在此期间一直处于运行状态,所以被称为自旋锁。自旋锁适用于锁持有时间非常短的情况,因为线程不会在等待锁的过程中被挂起,所以可以立即获取锁,避免了线程上下文切换的开销。
睡眠锁(Sleeping lock):当一个线程尝试获取一个已经被其他线程持有的睡眠锁时,该线程将被挂起(或者说“睡眠”),直到锁被释放。这种锁适用于锁持有时间较长的情况,因为它可以让出CPU给其他线程使用。
自旋锁的典型使用场景是低级别的系统代码,如操作系统内核。例如,在Linux内核中,自旋锁被用于保护任务队列。因为操作系统代码通常不能被挂起(因为它可能正在处理一个更高级别的中断),所以自旋锁是一个很好的选择。
睡眠锁的典型使用场景是用户级别的代码,如数据库系统。例如,一个数据库可能使用睡眠锁来保护对数据库表的访问。因为数据库操作可能需要一段时间来完成(例如,需要从磁盘读取数据),所以使用睡眠锁可以在等待期间让出CPU给其他线程使用。
睡眠锁使用示例
在C++中,std::mutex
是一种常用的睡眠锁。当一个线程试图获取一个已经被其他线程持有的std::mutex
时,它会被操作系统挂起,进入睡眠状态。在这个过程中,CPU可以被其他线程或进程使用,因为当前线程并没有执行任何操作。当锁被释放时,操作系统会唤醒等待的线程,让它再次尝试获取锁。
这个过程并不涉及到pause
操作。pause
是一种用于自旋锁的优化手段,它可以让当前线程暂时让出CPU,以减少CPU的使用率。但是,在睡眠锁中,线程在等待获取锁的过程中并不会执行任何操作,所以不需要pause
操作。
以下是一个使用std::mutex
的简单示例:
#include <mutex>
#include <thread>
std::mutex mtx;
void worker() {
mtx.lock();
// 执行一些操作...
mtx.unlock();
}
int main() {
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
return 0;
}
在这个示例中,worker
函数试图获取mtx
锁。如果mtx
已经被其他线程持有,那么当前线程会被挂起,进入睡眠状态,等待被唤醒。
总结
总结一下,自旋锁和睡眠锁的本质区别在于自旋锁在等待的过程中始终占有 CPU 不会切换上下文,而睡眠锁在等待的过程中不会占有 CPU 而是让 CPU 去执行其他任务等待条件满足后再唤醒睡眠锁。