原文:自旋锁
自旋锁(spinlock)是一种用于多线程同步的锁,主要用于保护共享资源或临界区,以防止多个线程同时访问同一资源。自旋锁与传统的互斥锁(如 POSIX 的互斥量)相比,在某些场景下可以提供更高效的锁定机制,特别是在锁持有时间极短的情况下。
工作原理
自旋锁的基本原理是当一个线程尝试获取一个已被另一个线程持有的锁时,该线程不会立即进入休眠状态(阻塞状态),而是在一个循环中"自旋",反复检查锁的状态。这意味着它会持续占用CPU资源,直到锁变得可用为止
自旋锁的特点
-
忙等待:线程获取自旋锁时,如果锁被占用,线程会持续循环并检查锁的状态,这种方式称为忙等待。因此,自旋锁不会使线程进入睡眠状态。
-
CPU资源消耗:由于忙等待,自旋锁在锁被长时间持有时会消耗大量的CPU资源。因此,它们最适用于锁持有时间极短的情况。
-
无上下文切换开销:相比于其他锁(如互斥锁),自旋锁在获取锁的过程中不涉及线程上下文的切换,这可以减少额外的调度开销。
-
避免死锁:自旋锁需要正确的管理来避免死锁,特别是在有多个自旋锁或与其他锁类型混合使用时。
使用场景
自旋锁特别适用于以下场景:
-
锁持有时间短:当预期锁只会被短时间持有时,自旋锁效率高,因为线程不会进入睡眠状态。
-
多核处理器:在单核处理器上使用自旋锁可能不太合适,因为忙等待会阻塞CPU唯一的核心。在多核处理器上,一个核心可以在获取锁的过程中自旋,而其他核心仍然可以继续执行其他任务。
-
实时系统:在需要快速响应的系统中,自旋锁由于没有线程休眠和唤醒的开销,可以提供更快的锁定操作。
实现示例
以下是在C++中使用自旋锁的简单示例,利用C++11标准中的atomic库来实现:#include <atomic>
#include <iostream>
#include <thread>
class SpinLock {
private:
std::atomic_flag lock_flag = ATOMIC_FLAG_INIT;
public:
void lock() {
while (lock_flag.test_and_set(std::memory_order_acquire)) {
// 忙等待,直到锁变为可用
}
}
void unlock() {
lock_flag.clear(std::memory_order_release);
}
};
SpinLock spin;
void task(const char* threadName) {
spin.lock();
std::cout << "Thread " << threadName << " entered critical section.\n";
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟工作
std::cout << "Thread " << threadName << " leaving critical section.\n";
spin.unlock();
}
int main() {
std::thread t1(task, "1");
std::thread t2(task, "2");
t1.join();
t2.join();
return 0;
}
上面的代码是最简单的自旋锁,异常情况下可能会一直占用cpu导致过度自旋。
怎么避免过度自旋呢?
改进的自旋锁:使用指数退避
在高度竞争的环境中,简单的自旋锁可能导致过多的CPU资源浪费。通过指数退避算法,可以减少自旋的冲突和CPU的压力。
示例代码(伪代码):
void lock() {
int delay = MIN_DELAY;
while (lock_flag.test_and_set(std::memory_order_acquire)) {
for (int i = 0; i < delay; i++) {
// Pause instruction to reduce power consumption
std::this_thread::yield(); // Yield to the scheduler
}
delay = std::min(delay * 2, MAX_DELAY); // Exponential backoff
}
}
在这个版本中,当线程无法立即获取锁时,它会等待一段时间,等待时间会指数级增加。
在这个版本中,当线程无法立即获取锁时,它会等待一段时间,等待时间会指数级增加。
票据自旋
票据自旋锁使用两个计数器:一个用于进入队列的票据,一个用于离开队列的票据。每个线程在进入时获取一个票据,并等待直到它的票据号到来。
示例代码(C++11):
#include <atomic>
class TicketSpinLock {
private:
std::atomic<int> service_number {0};
std::atomic<int> next_ticket {0};
public:
void lock() {
int my_ticket = next_ticket.fetch_add(1, std::memory_order_relaxed);
while (service_number.load(std::memory_order_acquire) != my_ticket) {
std::this_thread::yield();
}
}
void unlock() {
service_number.fetch_add(1, std::memory_order_release);
}
};
这种方式可以保证公平性,避免“饥饿”,因为获取锁的顺序完全按照请求锁的顺序。
伪共享避免优化
当多个线程频繁读写相邻的内存位置时,可能会因为CPU缓存系统的工作机制而导致不必要的性能损失。通过对自旋锁变量进行对齐和填充,可以避免这种情况。
示例修改(在C++中使用对齐):
#include <atomic>
class TicketSpinLock {
private:
std::atomic<int> service_number {0};
std::atomic<int> next_ticket {0};
public:
void lock() {
int my_ticket = next_ticket.fetch_add(1, std::memory_order_relaxed);
while (service_number.load(std::memory_order_acquire) != my_ticket) {
std::this_thread::yield();
}
}
void unlock() {
service_number.fetch_add(1, std::memory_order_release);
}
};
总结
自旋锁的选择和实现应基于应用场景的具体需求:锁的持有时间、线程数、锁的竞争程度等。在设计时考虑到这些因素,可以显著提高并发程序的性能和稳定性。在高度竞争的环境中,结合指数退避、票据机制或避免伪共享等策略,可以进一步优化自旋锁的效率。