Postgres中的SpinLock锁

我们知道,在数据库中为了并发控制,少不了要使用各种各样的锁(lock)。PostgreSQL中也不例外。 在PostgreSQL中有三种级别的锁,他们的关系如下: |上层 RegularLock | | LWLock | |底层 SpinLock 那么按照顺序,我们先来讨论下PostgreSQL的最底层的SpinLock。 作为PostgreSQL的最底层的锁,SpinLock比较简单,它的特点是封锁时间很短,没有等待队列和死锁检测机制,在事务结束时不能自动释放。因此,SpinLock一般不单独使用,而是作为其他锁(LWLock)的底层实现。 作为最底层锁,它的实现是和操作系统和硬件环境相关的。为此,PostgreSQL实现了两个SpinLock: 与机器相关的实现,利用TAS指令集实现(定义在s_lock.h和s_lock.c中); 与机器无关,利用PostgreSQL定义的信号量PGSemaphore实现(定义在spin.c中)。 很显然,依赖机器实现的SpinLock一定比不依赖机器实现的SpinLock要快。因此,如果PostgreSQL运行的机器上如果支持TAS指令集,那么自然会采用第一种实现,否则只能使用第二种实现了。 关于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) { ... 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自带的操作(在头文件中): 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。 注:本文还参考了这篇文章,在此表示感谢。 作者:非我在 出处:http://www.cnblogs.com/flying-tiger/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利. 感谢您的阅读。如果觉得有用的就请各位大神高抬贵手“推荐一下”吧!你的精神支持是博主强大的写作动力。 如果觉得我的博客有意思,欢迎点击首页左上角的“+加关注”按钮关注我!https://www.cnblogs.com/flying-tiger/p/9762886.html
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信