一、自旋锁简介
自旋锁(spinlock)是一种用于多线程或多进程环境中的同步机制,它用来保护共享资源,保证在同一时间内只有一个进程可用访问临界区,通常用于时间非常短的场景,如修改或读取标记字段(几十条指令之内),若锁需要持有超过几十条指令或者跨越任何内核调用时,不建议使用自旋锁。
openGauss 的 spinlock 采用 TAS 指令实现;锁状态只有两种:加锁和解锁;锁类型只有排他类型。
TAS 指令是指 Test And Set 指令,这是一种原子操作,用于多线程编程中的同步和锁机制,作用是检查一个内存位置(通常是一个字节或一个字)的值,并将其设置为一个新的值,所有这些操作在一个不可分割的原子步骤中完成。
TAS 指令实现自旋锁:当一个线程想要获取锁时,它会检查锁变量的值(通常 0 表示未锁定,1 表示已锁定),如果锁是未锁定状态(值是 0),线程就会将该值设置为 1 ,表示它已经获取了锁。如果锁已经被其他线程占用(值是 1),那么当前线程就会自旋,不断重试直到锁变为可用。
自旋锁的加锁和解锁需要保证在同一个函数内,由编码保证不会产生死锁。
这是因为自旋锁没有死锁检测且没有等待机制(无等待队列)。
自旋锁使线程处于忙等状态(busy-wait 循环)
二、自旋锁的工作过程
-
尝试获取锁
当一个线程想要进入临界区时,它会尝试获取自旋锁。这通过检查锁的状态(一个变量)来完成。如果锁是未锁定状态,线程会将其设置为已锁定状态,然后进入临界区。
-
自旋等待
如果锁已经被其他线程占用,尝试获取锁的线程不会立即阻塞或放弃 CPU 资源,而是进入一个忙等待(busy-wait)循环,也就是“自旋”。在循环中,线程会不断检查锁的状态,直到锁变为可用状态。
-
释放锁
当持有锁的线程完成对临界区的访问后,它会将锁的状态量重置为未锁定状态,从而允许其他线程获取锁。
自旋锁的优缺点
- 自旋锁的优点在于它们通常比阻塞型锁(需要上下文切换的锁)更快,因为它们不需要进行线程调度和上下文切换。
- 自旋锁的主要缺点是,如果锁被持有的时间较长,自旋的线程会浪费 CPU 资源,因为它一直在忙等待。这可能导致性能下降,尤其是在多核处理器上,自旋线程可能会阻塞其他线程的执行。当前 openGauss 中很多32/64/128 位变量的更新改为用 CAS 原子操作,避免或减少使用自旋锁。
三、自旋锁相关流程
路径:/src/include/storage/spin.h
/* 自旋锁的初始化 */
#define SpinLockInit(lock) S_INIT_LOCK(lock)
/* 自旋锁加锁 */
#define SpinLockAcquire(lock) S_LOCK(lock)
/* 自旋锁释放锁 */
#define SpinLockRelease(lock) S_UNLOCK(lock)
/* 自旋锁销毁并清理相关资源 */
#define SpinLockFree(lock) S_LOCK_FREE(lock)
自旋锁加锁
路径:/src/gausskernel/storage/lmgr/s_lock.cpp
尝试获取一个自旋锁,接受一个指向 volatile slock_t 类型的指针 Lock 表示要锁定的自旋锁,以及 file 和 line 参数,通常用于记录获取锁的位置(文件名和行号),以便调试和诊断。
/*
* s_lock(lock) - platform-independent portion of waiting for a spinlock.
*/
int s_lock(volatile slock_t* lock, const char* file, int line)
{
SpinDelayStatus delayStatus = init_spin_delay((void*)lock);
while (TAS_SPIN(lock)) {
perform_spin_delay(&delayStatus);
}
finish_spin_delay(&delayStatus);
return delayStatus.delays;
}
- 初始化自旋锁延迟状态 delayStatus。
- 循环尝试通过 TAS 操作获取锁,如果锁已经被其他线程持有,则返回非零值,表示获取锁失败。在循环内,如果获取锁失败,调用 finish_spin_delay 来处理自旋延迟,以减少 CPU 资源的消耗。
- 完成自旋延迟,更新统计信息或清理资源。
- 返回自旋延迟次数。
TAS 操作
路径:/src/include/gtm/gtm_slock.h
static __inline__ int tas(volatile slock_t* lock)
{
return __sync_lock_test_and_set(lock, 1);
}
finish_spin_delay
路径:/src/gausskernel/storage/lmgr/s_lock.cpp
/*
* Wait while spinning on a contended spinlock.
*/
void perform_spin_delay(SpinDelayStatus* status)
{
/* CPU 特定延迟 */
SPIN_DELAY();
/* 检查自旋次数是否达到阈值,如果达到,执行延迟操作 */
if (++(status->spins) >= t_thrd.storage_cxt.spins_per_delay) {
/* 如果延迟次数超过了最大延迟次数,报告自旋锁卡住 */
if (++(status->delays) > NUM_DELAYS)
s_lock_stuck(status->ptr, status->file, status->line);
/* 初始化延迟时间 */
if (status->cur_delay == 0) /* first time to delay? */
status->cur_delay = MIN_DELAY_USEC;
/* 执行延迟 */
pg_usleep(status->cur_delay);
/* 调试输出 */
#if defined(S_LOCK_TEST)
fprintf(stdout, "*");
fflush(stdout);
#endif
/* 增加延迟时间 */
status->cur_delay += (int)(status->cur_delay * ((double)random() / (double)MAX_RANDOM_VALUE) + 0.5);
/* 延迟时间上限检查 */
if (status->cur_delay > MAX_DELAY_USEC)
status->cur_delay = MIN_DELAY_USEC;
/* 重置自旋计数 */
status->spins = 0;
}
}
SpinDelayStatus 结构体定义
typedef struct {
int spins;
int delays;
int cur_delay;
void* ptr;
const char* file;
int line;
} SpinDelayStatus;
这个结构体包含以下成员:
- int spins:记录自旋操作的次数。
- int delays:记录延迟操作的次数。
- int cur_delay:记录当前的延迟值。
- void* ptr:指向触发自旋延迟的锁或资源的指针。
- const char* file:记录触发自旋延迟的代码文件名。
- int line:记录触发自旋延迟的代码行号。
这个结构体的设计目的是为了提供一种机制来跟踪和控制自旋锁操作中的延迟,以减少在高争用情况下对 CPU 的过度消耗。通过记录自旋和延迟的次数,开发者可以分析和优化锁的性能,特别是在性能敏感的应用中。使用 SpinDelayStatus 结构体时,通常会在尝试获取锁的循环中更新这些值,并在锁获取失败时增加延迟,以减少自旋的频率。这种方法有助于减少在锁竞争激烈时的 CPU 使用率,同时仍然保持对锁的快速响应。
作者:huan