postgresql内核分析 spinlock与lwlock原理与实现机制

专栏内容
postgresql内核源码分析
手写数据库toadb
并发编程
个人主页我的主页
座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物.

========================================

概述

在postgresql 中,有大量的并发同步,所以避免不了使用很多保护锁。
同时为了提升并发的性能,针对不同场景下的加锁需求,设计了:

  • spinlock 自旋锁
  • lightweight lock(LWLocks) 轻量级锁
  • regular lock(a/k/a heavyweight locks) 普通锁
  • SIReadLock predicate locks 谓词锁

本文主要针对这四种锁进行分享,起抛砖引玉的作用。

spinlock

是一种持有时间非常短的锁。它是通过test and set 原子操作来实现。

通过一定时间内的检测,如果没有持有就获得,这个时间大概是1min,超时就会导致ERR错误。
所以此类锁,都是一些状态保护,很快就释放,中间没有IO,大的内存操作。

它的实现依赖于操作系统的原子操作实现,所以通过宏定义共公接口,底层根据不同操作系统实现不同。

也可以说是一种无锁化的实现,需要原子操作TAS和内存同步。

操作函数

#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)

底层操作函数有这四个,是通过宏定义给出,对于不同操作系统下,定义了具体的原子操作。
在支持TAS 原语的操作系统上,用TAS来实现,如加锁的函数如下

int s_lock(volatile slock_t *lock, const char *file, int line, const char *func)
{
    
    
	SpinDelayStatus delayStatus;

	init_spin_delay(&delayStatus, file, line, func);

	while (TAS_SPIN(lock))
	{
    
    
		perform_spin_delay(&delayStatus);
	}

	finish_spin_delay(&delayStatus);

	return delayStatus.delays;
}

#define TAS_SPIN(lock)    (*(lock) ? 1 : TAS(lock))

static __inline__ int
tas(volatile slock_t *lock)
{
    
    
	slock_t		_res = 1;

	__asm__ __volatile__(
		"	lock			\n"
		"	xchgb	%0,%1	\n"
:		"+q"(_res), "+m"(*lock)
:		/* no inputs */
:		"memory", "cc");
	return (int) _res;
}

可以看到核心代码是通过汇编实现TAS操作,大致流程是这样:

  1. 检测lock是否为0 ,如果不为0,说明还没有解锁,继续等,直到超时;
  2. 如果已经解锁,就走入汇编代码;锁定总线,通过xchgb 原子交换lock和_res=1 两个值,进行内存同步;加锁成功;
  3. 此时TAS_PIN返回0,等待结束;

而slock_t 是什么类型呢?
如果在支持TAS指令的操作系统下是如下定义

typedef unsigned char slock_t;

是一个字节,这样可以很快的检测和原子交换赋值

注意事项

通过上面的原理介绍,可以看到它等待的时间非常短,这就是说在锁持有时,不能占用太久时间。

因此,在持有spinlock时,只是一些状态的获取和赋值,就要立即释放,否则就会有大量超时。
在锁持有此间,避免磁盘,网络,函数调用等其它额外操作。

轻量级锁 lightweight lock

介绍

轻量级锁将加锁过程分成了两个阶段,第一阶段通过原子操作来检测,如果可以加锁,就加锁成功;如果不能加锁,进入第二阶段,将自己加入等待队列,并阻塞在信号量上;

主要用于共享内存和数据块的操作保护

它因为分了两个阶段,所以较一般的系统级锁性能更高效一些。
它提供了如下特点:

  • 能够快速检测锁状态,并且获取到锁;
  • 每个后台进程只能有一个排队中的轻量级锁;
  • 在持有锁期间,信号会被阻塞
  • 在错误时会释放锁;

数据结构

typedef struct LWLock
{
    
    
	uint16		tranche;		/* tranche ID */
	pg_atomic_uint32 state;		/* state of exclusive/nonexclusive lockers */
	proclist_head waiters;		/* list of waiting PGPROCs */
#ifdef LOCK_DEBUG
	pg_atomic_uint32 nwaiters;	/* number of waiters */
	struct PGPROC *owner;		/* last exclusive owner of the lock */
#endif
} LWLock;

extern bool LWLockAcquire(LWLock *lock, LWLockMode mode);
extern bool LWLockConditionalAcquire(LWLock *lock, LWLockMode mode);
extern bool LWLockAcquireOrWait(LWLock *lock, LWLockMode mode);
extern void LWLockRelease(LWLock *lock);

初始化

加锁

  • 判断是否已经持有锁数量,超过上限;阻塞信号中断;
  • 第一阶段 尝试加锁,加上时直接返回锁;否则将自己放入等待队列;再次尝试加锁;
  • 第二阶段 如果仍没有获取到锁时,在当前backend对应的 MyProc中的信号量上进行等待;

直到被唤醒,如果proc->lwWaiting == LW_WS_NOT_WAITING时,继续等待;

  • 当获取到锁时,将锁加入自己持有锁的数组中记录;

解锁

从本等数据中获取当前锁的加锁模式; 从锁中解除;
如果有等待者,将它们从等待队列中移除,然后唤醒它们;等待者们将再次竞争;

等待锁释放

bool
LWLockAcquireOrWait(LWLock *lock, LWLockMode mode);
  • 介绍

这个接口有点意思,即可以获取锁,也用来等待别人释放锁;

当前锁如果没有被占用,则占有锁后函数返回;
如果当前锁被占用,则等待锁,等别人释放锁后,就直接返回,而不持有锁。

  • 用途

这个函数主要用来在写WAL时,获取锁,因为同时只能有一个进程写WAL;
如果当前没有人写WAL,则持有锁后,执行WAL写入。
如果当前已经有人持有锁,在写WAL,那么自己的WAL也会被写入,因为WAL是顺序写入,后写时,需要把前面的内容都要写入。

条件变量

static bool LWLockConflictsWithVar(LWLock *lock,
					   uint64 *valptr, uint64 oldval, uint64 *newval,
					   bool *result)
bool LWLockWaitForVar(LWLock *lock, uint64 *valptr, uint64 oldval, uint64 *newval);
void LWLockUpdateVar(LWLock *lock, uint64 *valptr, uint64 val);

基于轻量级锁,又实现了一组类似于条件变量的接口;

LWLockWaitForVar检测变量是否变化,如果没人持有锁,那就直接返回;如果有锁,则等待,直到锁释放后,返回新值;
LWLockUpdateVar是改变变量的值,并通知等待者,唤醒等待者;

锁排队

lightweiht lock可能会长时间等待,因此每个backend只能有一个正在等待的轻量级锁,所以每个backend都会有一个信号量;

struct PGPROC
{
    
    
	// other members ... 
	PGSemaphore sem;			/* ONE semaphore to sleep on */
	// other members ... 
};

信号量定义在PROC结构上,当进入信号量等待时,同时也会把自己的MyProc添加到 lock->waiters 列表成员中。

在锁持有者释放锁时,会删除队列中的所有成员,同时唤醒等待者的信号量;

在介绍了排队和释放后,就会发现它存在两个问题:

  • 等锁的饿死问题
  • 惊群问题

当然lwlock 队列的唤醒也是顺序唤醒,同时加锁分为两阶段,这就在一定程度上避免了上述问题。

另外lwlock加锁是非常频,可能在很短时间有加锁/释放,所以需要更简洁直接的加锁方式。

结尾

非常感谢大家的支持,在浏览的同时别忘了留下您宝贵的评论,如果觉得值得鼓励,请点赞,收藏,我会更加努力!

作者邮箱:[email protected]
如有错误或者疏漏欢迎指出,互相学习。

注:未经同意,不得转载!

猜你喜欢

转载自blog.csdn.net/senllang/article/details/131499534