Postgres中的SpinLock锁

咱们知道,在数据库中为了并发控制,少不了要使用各类各样的锁(lock)。PostgreSQL中也不例外。算法

在PostgreSQL中有三种级别的锁,他们的关系以下:数据库

|上层  RegularLock
  |
  |      LWLock
  |
  |底层  SpinLock

那么按照顺序,咱们先来讨论下PostgreSQL的最底层的SpinLock。编程

做为PostgreSQL的最底层的锁,SpinLock比较简单,它的特色是封锁时间很短,没有等待队列和死锁检测机制,在事务结束时不能自动释放。所以,SpinLock通常不单独使用,而是做为其余锁(LWLock)的底层实现。windows

做为最底层锁,它的实现是和操做系统和硬件环境相关的。为此,PostgreSQL实现了两个SpinLock:多线程

  • 与机器相关的实现,利用TAS指令集实现(定义在s_lock.h和s_lock.c中);并发

  • 与机器无关,利用PostgreSQL定义的信号量PGSemaphore实现(定义在spin.c中)。app

很显然,依赖机器实现的SpinLock必定比不依赖机器实现的SpinLock要快。所以,若是PostgreSQL运行的机器上若是支持TAS指令集,那么天然会采用第一种实现,不然只能使用第二种实现了。ide

关于SpinLock的动做,能够看下面这张图:函数


###机器相关的实现###优化

咱们,知道与机器相关的实现利用了TAS指令集。那么什么是TAS呢?

TAS是 Test and Set的缩写。是一个原子操做。它修改内存的值,并返回原来的值。当一个进程P1对一个内存位置作TAS操做,不容许其它进程P2对此内存位置再作TAS操做。P2必须等P1操做完成后,再作TAS操做。所以,该操做被用来实现进程互斥。

有了这个概念,咱们来看源代码。

代码在:

src/include/storage/s_lock.h
src/backend/storage/lmgr/s_lock.c

虽说了对于SpinLock有两个底层实现,可是在上层调用时,咱们是使用统一的接口的,接口在src/backend/storage/lmgr/s_lock.c中:

/*
 * s_lock(lock) - platform-independent portion of waiting for a spinlock.
 */
int
s_lock(volatile slock_t *lock, const char *file, int line, const char *func)
{
    ...
   
	while (TAS_SPIN(lock))   //调用点
	{
    
    ...	

}

能够发现这个TAS_SPIN(lock)是一个宏,

#define TAS_SPIN(lock)	TAS(lock)

当使用基于TAS指令集的锁时,有:

#define TAS(lock) tas(lock)

对机器的TAS的使用在函数tas()中。

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

	/*
	 * Use a non-locking test before asserting the bus lock.  Note that the
	 * extra test appears to be a small loss on some x86 platforms and a small
	 * win on others; it's by no means clear that we should keep it.
	 *
	 * When this was last tested, we didn't have separate TAS() and TAS_SPIN()
	 * macros.  Nowadays it probably would be better to do a non-locking test
	 * in TAS_SPIN() but not in TAS(), like on x86_64, but no-one's done the
	 * testing to verify that.  Without some empirical evidence, better to
	 * leave it alone.
	 */
	__asm__ __volatile__(
		"	cmpb	$0,%1	\n"
		"	jne		1f		\n"
		"	lock			\n"
		"	xchgb	%0,%1	\n"
		"1: \n"
:		"+q"(_res), "+m"(*lock)
:		/* no inputs */
:		"memory", "cc");
	return (int) _res;
}

能够看到这段在C语言中的内嵌汇编代码便是调用了机器的TAS指令。假设lock原来的值为“0”,当P1去作申请lock时,能获取获得锁。而此时P2再去申请锁时,必须spin,由于此时lock的值已经被P1修改成“1”了。

用TAS来实现spin lock,此处要注意volatile的使用。volatile表示这个变量是易失的,因此会编译器会每次都去内存中取原始值,而不是直接拿寄存器中的值。

这避免了在多线程编程中,因为多个线程更新同一个变动,内存中和寄存器中值的不一样步而致使变量的值错乱的问题。另外,也会影响编译器的优化行为。

具体汇编代码的解析,能够查看相关资料。

在使用时,PostgreSQL不直接调用tas()函数,而是经过:

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

来申请spin lock。返回值是等待的时间。


###机器无关的实现### 若是机器上没有TAS指令集,那么PostgreSQL利用PGSemaphores来实现SpinLock。

PGSemaphore是使用OS底层的semaphore来实现的,PG对其作了封装,提供了PG系统内部统一的semaphore操做接口。PG的用PGSemaphore结构体表示PG自身的semaphore信号,并将相关操做封装在sembuf中,传递给底层OS。

实现代码在:

src/backend/storage/lmgr/spin.c

咱们知道这个TAS_SPIN(lock)是SpinLock的抽象定义:

#define TAS_SPIN(lock)	TAS(lock)

在不使用TAS的场合,有:

#define TAS(lock)	tas_sema(lock)

即调用tas_sema(lock)函数实现SpinLock:

int
tas_sema(volatile slock_t *lock)
{
	/* Note that TAS macros return 0 if *success* */
	return !PGSemaphoreTryLock(&SpinlockSemaArray[*lock]);
}

对于信号量,PostgreSQL分别针对POSIX 信号量、SYSTEM V信号量和windows信号量进行了不一样的实现,实现代码分别在:

src/backend/port/posix_sema.c
src/backend/port/sysv_sema.c
src/backend/port/win32_sema.c

咱们这里以SYSTEM V信号量为例进行讲解。

PGSemaphoreTryLock的定义为:

bool
PGSemaphoreTryLock(PGSemaphore sema)
{
	int			errStatus;
	struct sembuf sops;    //重要!!!

	sops.sem_op = -1;			/* decrement */
	sops.sem_flg = IPC_NOWAIT;	/* but don't block */
	sops.sem_num = sema->semNum;

	/*
	 * Note: if errStatus is -1 and errno == EINTR then it means we returned
	 * from the operation prematurely because we were sent a signal.  So we
	 * try and lock the semaphore again.
	 */
	do
	{
		errStatus = semop(sema->semId, &sops, 1);
	} while (errStatus < 0 && errno == EINTR);
	
	...

即调用了PGSemaphores来实现SpinLock。

而PGSemaphores的定义为:

typedef struct PGSemaphoreData
{
	int			semId;			/* semaphore set identifier */
	int			semNum;			/* semaphore number within set */
} PGSemaphoreData;

在利用system V信号量时,咱们有:

struct sembuf
{
unsigned short int sem_num; /* semaphore number */
short int sem_op; /* semaphore operation */
short int sem_flg; /* operation flag */
};

PGSemaphoreTryLock中的while循环里就是执行了semop操做。 而这些操做是OS自带的操做(在<sys/sem.h>头文件中):

extern int semop(int __semid, struct sembuf *opsptr, size_t nops);

很明显,此处PostgreSQL封装了OS底层的system V 的semaphore,而后利用OS底层的系统函数来操做。

剩下两种信号量大抵如此,此处很少言。


###共通的操做### SpinLock是分两种状况来分别实现的。这是它们的不一样,在Spinlock之上有一些共通的操做要说明下。对于SpinLock的获取,并非每次都成功,当尝试获取时发现一个对象已经被lock时,当前线程不会阻塞在改锁上,而是先spin(自旋)必定的次数以后再sleep必定的时间后尝试再次获取。对于每次spin以后的sleep时间,PostgreSQL使用了自适应算法,来决定spin的次数和每次spin后,sleep的时间。

下面两个变量要注意下:

spins_per_delay

该变量表示spin多少次后,开始sleep。默认为100,最大值为1000,最小值为10。

spins_per_delay的值基本上不变;可是cur_delay的值为当前值1倍和2倍之间变更。所以,spin delay次数越多,sleep时间会越长。

还有一个变量:

cur_delay

当前sleep的时间,最大值为1000,最小值为1。单位为ms。


###小结###

本文讨论了关于PostgreSQL的SpinLock实现以及相关函数。SpinLock是PostgreSQL的最底层的锁,它的主要做用是为上层的锁提供支持。本文SpinLock就聊到这里,下次咱们来聊PostGreSQL的LWLock和RegularLock。

注:本文还参考了这篇文章,在此表示感谢。

相关文章
相关标签/搜索