专栏内容:
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操作,大致流程是这样:
- 检测lock是否为0 ,如果不为0,说明还没有解锁,继续等,直到超时;
- 如果已经解锁,就走入汇编代码;锁定总线,通过xchgb 原子交换lock和_res=1 两个值,进行内存同步;加锁成功;
- 此时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加锁是非常频,可能在很短时间有加锁/释放,所以需要更简洁直接的加锁方式。
结尾
非常感谢大家的支持,在浏览的同时别忘了留下您宝贵的评论,如果觉得值得鼓励,请点赞,收藏,我会更加努力!
作者邮箱:study@senllang.onaliyun.com
如有错误或者疏漏欢迎指出,互相学习。
注:未经同意,不得转载!