Postgresql中使用CAS实现无锁队列

2023-03-01 13:33:09 浏览数 (1)

概述

Postgresql中的大锁很多,其中ProcArrayLock和XactSLRULock使用了无锁队列优化(PG中XID的分发也可以用这种机制优化,高压场景下效果不错)。

  • ProcArrayLock
    • 优化前:事务提交清理proc的xid,所有进程争抢ProcArrayLock(PG的锁机制会用sem排队)。
    • 优化后:第一个向队列加入元素的proc为leader,后续的proc使用cas将要做的加入队列,由leader统一做。
  • XactSLRULock
    • 优化前:写CLOG时,不同进程争抢XactSLRULock(PG的锁机制会用sem排队),拿到锁才有资格写SLRU页面。
    • 优化后:第一个向队列加入元素的proc为leader,后续的proc使用cas将要做的加入队列,由leader统一做。

关于XactSLRULock请参考《Postgresql源码(101)深入分析clog组提交(clog group updates)》

本篇只精简CAS部分的实现,具体请参考上面文章。

Postgresql中的CAS无锁队列逻辑总结

PG中CAS无锁队列最简代码(所有变量为int)

代码语言:javascript复制
ProcArrayGroupClearXid
    ...
    ...
	nextidx = pg_atomic_read_u32(&procglobal->procArrayGroupFirst);
	while (true)
	{
		pg_atomic_write_u32(&proc->procArrayGroupNext, nextidx);

		if (pg_atomic_compare_exchange_u32(&procglobal->procArrayGroupFirst,
										   &nextidx,
										   (uint32) proc->pgprocno))
			break;
	}

pg_atomic_compare_exchange_u32为PG的CAS实现,在X86下使用汇编lock锁总线cmpxchgl实现:

代码语言:javascript复制
static inline bool
pg_atomic_compare_exchange_u32_impl(volatile pg_atomic_uint32 *ptr,
									uint32 *expected, uint32 newval)
{
	char	ret;

	/*
	 * Perform cmpxchg and use the zero flag which it implicitly sets when
	 * equal to measure the success.
	 */
	__asm__ __volatile__(
		"	lock				n"
		"	cmpxchgl	%4,%5	n"
		"   setz		%2		n"
:		"=a" (*expected), "=m"(ptr->value), "=q" (ret)
:		"a" (*expected), "r" (newval), "m"(ptr->value)
:		"memory", "cc");
	return (bool) ret;
}

pg_atomic_compare_exchange_u32的逻辑比较简单:

  1. 1参一定赋值给2参nextidx:nextidx = procArrayGroupFirst,这步非常重要,2参nextidx每次都被更新为共享内存中的新值(并发被别人修改了),后续可以在新的基础上做操作。
  2. 3参可能赋值给1参procArrayGroupFirst:procArrayGroupFirst = pgprocno(如果1参等于2参)

实例

考虑ProcArrayGroupClearXid三并发进入ProcArrayGroupClearXid函数的场景:

进程一:进入前

代码语言:javascript复制
procArrayGroupFirst  -----> invalid <----- nextidx

第一次进入CAS nextidx和procArrayGroupFirst都等于invalid,所以CAS返回true,并且参1procArrayGroupFirst更新为procno1。(nextidx记录procArrayGroupFirst的旧值,也就是队列原来的头部)

代码语言:javascript复制
                            invalid <----- nextidx
                               ^
                               | procArrayGroupNext 
                               |               
procArrayGroupFirst  -----> procno1

进程二:CAS后

代码语言:javascript复制
                            invalid
                               ^
                               | procArrayGroupNext 
                               |               
                            procno1  <----- nextidx
                               ^
                               | procArrayGroupNext 
                               |               
procArrayGroupFirst  -----> procno2

进程三:CAS

代码语言:javascript复制
                            invalid
                               ^
                               | procArrayGroupNext 
                               |               
                            procno1
                               ^
                               | procArrayGroupNext 
                               |               
                            procno2  <----- nextidx
                               ^
                               | procArrayGroupNext 
                               |               
procArrayGroupFirst  -----> procno2

这样就通过CAS形成了一个无锁队列,其中队列头指针procArrayGroupFirst,队列关系由procArrayGroupNext串联,nextidx指向队列前一个元素。

因为CAS保证了compare 和 swap的原子性,所以pg_atomic_compare_exchange_u32不会出现以下场景:

  1. 比较完了相同,但swap时被别的进程改为不同了。
  2. 比较完了不同,但swap时被别的进程改为相同了。

综上,就是Postgresql中使用CAS实现无锁队列的方法。

0 人点赞