我们知道,在数据库中为了并发控制,少不了要使用各种各样的锁(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, 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。

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

Postgres中的SpinLock锁的更多相关文章

  1. Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等

    Java 中15种锁的介绍 Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等,在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类 ...

  2. 操作系统下spinlock锁解析、模拟及损耗分析

    关于spinlock 我们在知道什么是spinlock之前,还需要知道为什么需要这个spinlock?spinlock本质就是锁,提到锁,我们就回到了多线程编程的混沌初期,为了实现多线程编程,操作系统 ...

  3. 浅谈Linux中的各种锁及其基本原理

    本文首发于:https://mp.weixin.qq.com/s/Ahb4QOnxvb2RpCJ3o7RNwg 微信公众号:后端技术指南针 0.概述 通过本文将了解到如下内容: Linux系统的并行性 ...

  4. Linux中的各种锁及其基本原理

    Linux中的各种锁及其基本原理 1.概述 通过本文将了解到如下内容: Linux系统的并行性特征 互斥和同步机制 Linux中常用锁的基本特性 互斥锁和条件变量 2.Linux的并行性特征 Linu ...

  5. pgsql中的行锁

    pgsql中的行锁 前言 用户可见的锁 regular Lock 行级别 FOR UPDATE FOR NO KEY UPDATE FOR SHARE FOR KEY SHARE 测试下加锁之后的数据 ...

  6. Java中15种锁的介绍

    作者:搜云库技术团队 原文:https://segmentfault.com/a/1190000017766364 1. Java 中15种锁的介绍 在读很多并发文章中,会提及各种各样锁如公平锁,乐观 ...

  7. 【C# 锁】 SpinLock锁 详细分析(包括内部代码)

    OverView 同步基元分为用户模式和内核模式 用户模式:Iterlocked.Exchange(互锁).SpinLocked(自旋锁).易变构造(volatile关键字.volatile类.Thr ...

  8. postgres中几个复杂的sql语句

    postgres中几个复杂的sql语句 需求一 需要获取一个问题列表,这个问题列表的排序方式是分为两个部分,第一部分是一个已有的数组[0,579489,579482,579453,561983,561 ...

  9. postgres中的中文分词zhparser

    postgres中的中文分词zhparser postgres中的中文分词方法 基本查了下网络,postgres的中文分词大概有两种方法: Bamboo zhparser 其中的Bamboo安装和使用 ...

随机推荐

  1. C语言压缩/解压缩

    一.简介 Lzlib 压缩库提供了在内存中的 LZMA 压缩和解压算法功能,包括对数据进行完整性检查.压缩格式是 lzip 参考: http://blog.csdn.net/damenhanter/a ...

  2. Python爬虫进阶五之多线程的用法

    前言 我们之前写的爬虫都是单个线程的?这怎么够?一旦一个地方卡到不动了,那不就永远等待下去了?为此我们可以使用多线程或者多进程来处理. 首先声明一点! 多线程和多进程是不一样的!一个是 thread ...

  3. iOS Orientation获取

    [iOS Orientation获取] 1.[[UIDevice sharedInstance] orientation] 必须调用beginGeneratingDeviceOrientationNo ...

  4. typedef char int8; 这样定义的好处?

    用typedef定义int8代表char:然后用int8去定义其他变量.一旦系统中char不再是占8位的数据时,可重新typedef新的占8位的类型为int8,而所有的用int8定义的8为类型数不用再 ...

  5. edis.clients.jedis.exceptions.JedisDataException: MISCONF Redis is configured to save RDB snapshots,

    edis.clients.jedis.exceptions.JedisDataException: MISCONF Redis is configured to save RDB snapshots ...

  6. Hadoop学习【一】单机版搭建

    首先要说一下,Hadoop 2.x版本以后的改动,在这里帖一篇文章,觉得写的不错. http://www.ibm.com/developerworks/cn/opensource/os-cn-hado ...

  7. 对ConditionQueue和锁的理解

    1. 什么时候使用conditionQueue 使用conditionQueue的一个最基本的条件是,操作和状态相关,而且是多线程同时访问的状态. 也就是说在使用conditionQueue的时候, ...

  8. C#基础入门 七

    C#基础入门 七 接口 由于C#语言不支持多重继承,所以可以使用接口模拟结构的继承,通过使用interface关键字,定义一个接口. interface USB { void Read(string[ ...

  9. Postgresql fillfactor

    一个表的填充因子(fillfactor)是一个介于 10 和 100 之间的百分数.100(完全填充)是默认值.如果指定了较小的填充因子,INSERT 操作仅按照填充因子指定的百分率填充表页.每个页上 ...

  10. CentOS将普通用户加入管理员组

    将用户username加入wheel组: usermod -aG wheel username 将普通用户username加入root组: usermod -aG sudo username 永久生效 ...