我们知道,在数据库中为了并发控制,少不了要使用各种各样的锁(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. 图是否是树 · Graph Valid Tree

    [抄题]: 给出 n 个节点,标号分别从 0 到 n - 1 并且给出一个 无向边的列表 (给出每条边的两个顶点), 写一个函数去判断这张`无向`图是否是一棵树. 给出n = 5 并且 edges = ...

  2. WebSocket 资料搜索

    http://jwebsocket.org/ http://zh.wikipedia.org/wiki/WebSocket http://www.infoq.com/cn/news/2013/07/e ...

  3. python 开发简单的聊天工具-乾颐堂

    python 太强大了,以至于它什么都可以做,哈哈,开个玩笑.但是今天要讲的真的是一个非常神奇的应用. 使用python写一个聊天工具 其实大家平时用的QQ类似的聊天工具,也是使用socket进行聊天 ...

  4. Java Thread系列(十)生产者消费者模式

    Java Thread系列(十)生产者消费者模式 生产者消费者问题(producer-consumer problem),是一个多线程同步问题的经典案例.该问题描述了两个共亨固定大小缓冲区的线程-即所 ...

  5. SQLServer函数 left()、charindex()、stuff()的使用

    1.left() LEFT (<character_expression>, <integer_expression>)   返回character_expression 左起 ...

  6. jquery怎么根据后台传过来的值动态设置下拉框、单选框选中

    $(function(){ var sex=$("#sex").val(); var marriageStatus=$("#marriageStatus").v ...

  7. SpringMVC源码解析- HandlerAdapter初始化

    HandlerAdapter初始化时,主要是进行注解解析器初始化注册;返回值处理类初始化;全局注解@ControllerAdvice内容读取并缓存. 目录: 注解解析器初始化注册:@ModelAttr ...

  8. Configuring Oracle E-Business Suite Integrated SOA Gateway Release 12.1.2 and Release 12.1.3 in a Multinode Environment (Doc ID 1081100.1)

    Configuring Oracle E-Business Suite Integrated SOA Gateway Release 12.1.2 and Release 12.1.3 in a Mu ...

  9. C#基础入门 五

    C#基础入门 五 递归 递归调用:一个方法直接或间接地调用了它本身,就称为方法的递归调用. 递归方法:在方法体内调用该方法本身. 递归示例 public long Fib(int n) { if(n= ...

  10. Mirth Connect的简单使用

    第一步: 切换到Channels界面,右键点击New Channel 第二步 : 上面是设置一些通道信息. 其中summary(概要) 界面主要包含 通道名称,数据类型,依赖,通道初始状态,附件(是否 ...