PostgreSQL中的锁

  • 主要讲解PostgreSQL中所使用的锁及其原理

  PostgreSQL中关于spinLock的描述被放在spin.h和spin.c中。spin.h 中为硬件(平台)无关spinlock的实现的描述.

在实现时尤其需要注意是volatile 类型的使用;在使用该类型时候,在变量在内存中发生变化时候,不能够在使用该限定符来修饰变量,对于其中原因见:Lock-free Queue Impl中对于cmpxchg函数的描述。 其会产生ABA问题。

在使用spinlock的时候,为了防止编译器在优化过程中对相关代码优化导致的re-order问题[因为编译器在进行代码优化的过程中会将某些代码视作无用代码进行优化] ,因此我们在使用spinlock或者是其保护的共享内存对象时候,都应该使用volatile限定符对变量进行修饰。

但对于TAS指令的使用时候,对于某些系统结构上(weak memory ordering 弱内存序化)必须使用硬件层的memory fence指令来保证,指令的正确的顺序,因为在某些系统架构上(例如:alpha平台上)会导致代码的乱序执行。

wikipedia 上给出了对于memory ordering 的解释:

“Memory ordering is a group of properties of the modern microprocessors, characterising their possibilities in memory operations reordering. It is a type of out-of-order execution. Memory reordering can be used to fully utilize different cache and memory banks.

On most modern uniprocessors memory operations are not executed in the order specified by the program code. But in singlethreaded programs from the programmer’s point of view, all operations appear to have been executed in the order specified, with all inconsistencies hidden by hardware.”

该段话阐述了:对于多处理器环境下为了更多的使用多处理器的并行能力,一般会将程序分派到不同的处理器上执行,这便会导致程序的代码并不是我们所看到的顺序执行,而是某些独立的代码先于其它代码执行,当这些代码不存在着数据关联或者逻辑上的关联,此时便可以将这些代码分派到其它的处理器上执行;但是,当我们在单处理器上时候,编译器的优化则会少的很多,从程序猿的角度来看就像其代码是按照顺序执行的,但是所有的不一致性均被硬件层所规避。

更多参考:

1: Handling Memory Ordering in Multithreaded Applications with Oracle Solaris Studio 12

2:Weak vs. Strong Memory Models.

3: Memory ordering.

4:Memory ordering in Modern MicroProccessor.

在s_lock.h 中定义了硬件相关的spinlock的实现,在该文件中便使用了如下的代码 :

#if defined(__GNUC__) || defined(__INTEL_COMPILER)

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.
*/
__asm__ __volatile__(
” cmpb $0,%1 \n”
” jne 1f \n”
” lock \n”
” xchgb %0,%1 \n”
“1: \n”
: “+q”(_res), “+m”(*lock)
:
: “memory”, “cc”);
return (int) _res;
}

#else

static __inline__ int
tas(volatile slock_t *lock)
{
int ret;
ret = _InterlockedExchange(lock,1); /* this is a xchg asm macro */
return ret;
}

#endif

static __inline__ void
spin_delay(void)
{
__asm__ __volatile__(
” rep; nop \n”);
}

或者spin_delay被定义为: SPIN_DELAY() ((void) 0), 其仅仅是一个0,插入一条空指令,其作用类似于硬件指令中的 nop 指令。 同时,由于tas被大量的重复使用因此这些函数被定义为inline。

在s_lock.c 中我们看到s_lock函数 其原型定义如下:
int
s_lock(volatile slock_t *lock, const char *file, int line)
{

}

其中,slock_t 被定义为 typedef LONG slock_t;
其具体是实现为:
while (TAS_SPIN(lock))
{
SPIN_DELAY();
//进行尝试, 每次尝试后,增加尝试计数,并增大延迟时间。
if (++spins >= spins_per_delay) {
….
pg_usleep(cur_delay * 1000L);
….
}
}

至此,我们已经在硬件层上封装了一个锁的原始函数[硬件依赖],但是为了方便用户使用我们在其上面进行在封装,以便用户可以方便使用,其便是 spin.h 和spin.c 存在的原因。 在spin.h中我们定义了如下的四类宏:

#define SpinLockInit(lock) S_INIT_LOCK(lock)
#define SpinLockAcquire(lock) S_LOCK(lock)
#define SpinLockRelease(lock) S_UNLOCK(lock)
#define SpinLockFree(lock) S_LOCK_FREE(lock)

我们很容易从字面上可以看出, SpinLockInit对一个锁对象进行初始化操作, SpinLockAcquire获取一个锁对象,SpinLockRelease释放一个已经获得的锁对象, SpinLockFree释放相应的锁对象。 而这些宏所对于的实际的操作均定义在s_lock.h中。
但是在spin.c中我们也看到了当系统中已经定义了spinlock时候,即我们已经在s_lock.h/s_lock.c中完成了对锁底层的实现时,系统便使用该种方式,否则pg则使用semaphore来模拟锁的实现。 我们在spin.c中可以看到:

#ifdef HAVE_SPINLOCKS
xxx
#else
/*
* No TAS, so spinlocks are implemented as PGSemaphores.
*/
#endif

至于什么是信号量(Semaphore)请参考相关文档,这里不再详述。

下面我们讨论对于spinlock的使用场景,spinlock属于轻量级的锁,适用于对某个share object操作较多,并且占用时间不长的情景下使用,即我们在较小的代码中使用,对于复杂场景下的使用我们一般不采用spinlock,而采取其他方式。实例代码如下:

static void
BeginStrongLockAcquire(LOCALLOCK *locallock, uint32 fasthashcode)
{
SpinLockAcquire(&FastPathStrongRelationLocks->mutex);
FastPathStrongRelationLocks->count[fasthashcode]++;
locallock->holdsStrongLockCount = TRUE;
StrongLockInProgress = locallock;
SpinLockRelease(&FastPathStrongRelationLocks->mutex);
}

该段代码为PostgreSQL中的关于 PostGRS的primary lock的具体实现。其所采用的底层为我们上面所讨论的spinlock,我们可以说PG中的锁均以spinlock为基础,进而在此基础上所完成。

LigthWeight lock我们称为轻量锁,其主要是用来保护share-memory的数据结构,下面是PG对于Lwlock的说明,从中我们可以清楚的看到该种类型的锁的目的和作用,而这lwlock也是PostgreSQL所提供的锁的一种类型。其相应的代码分别在:lwlock.h/lwlock.c文件中。

“Lightweight locks are intended primarily to provide mutual exclusion of access to shared-memory data structures. Therefore, they offer both exclusive and shared lock modes (to support read/write and read-only access to a shared object). There are few other frammishes. User-level locking should be done with the full lock manager — which depends on LWLocks to protect its shared state. ”

Lwlock在实现的过程中使用了spinlock 作为其底层的实现机制。 在PostgreSQL中我们使用LWLock来描述轻量锁。 其相应的数据结构定义如下:

typedef struct LWLock
{
slock_t mutex; /* Protects LWLock and queue of PGPROCs */
bool releaseOK; /* T if ok to release waiters */
char exclusive; /* # of exclusive holders (0 or 1) */
int shared; /* # of shared holders (0..MaxBackends) */
PGPROC *head; /* head of list of waiting PGPROCs */
PGPROC *tail; /* tail of list of waiting PGPROCs, tail is undefined when head is NULL */

} LWLock;

从其数据结构的定义可以看出,其使用一个链表( PGPROC *head ,PGPROC *tail )来保存那些正在争取锁,而暂时无法得到满足的进程,我们称之为 Waiting List, 同时使用shared来描述先在共同持有该锁的进程数。Lwlock在所有已知的平台下其大小在16-32个字节。

为了系统在访问lwlock时的速度,我们将lwlock的大小限制为2的幂,这样在内存中可以进行对其从而加快其访问速度;同时,保证每个lwlock对象不会跨越cache边界,因而可以降低了cache竞争;故此,我们将lwlock进行包装,使用一些pad来进行填充,使其满足大小为2的幂。 其方式如下:

#define LWLOCK_PADDED_SIZE (sizeof(LWLock) <= 16 ? 16 : 32)

typedef union LWLockPadded
{
LWLock lock;
char pad[LWLOCK_PADDED_SIZE];
} LWLockPadded;

为了方便的跟踪我们所持有锁的情况,我们使用一个数组来保存我们说持有的锁的ID,其中 num_held_lwlocks 描述了我们所持有lwlock的数量,而数组held_lwlocks中描述了我们所持有的所的情况[我们根据其锁的ID来获取相应的锁对象]。我们可以使用print_lwlock_stats 函数来打印出每个所的具体状态信息,这里我们就不在详述该函数的具体实现,有兴趣可以阅读相应的实现[lwlock.c]。

#define MAX_SIMUL_LWLOCKS 100
static int num_held_lwlocks = 0;
static LWLockId held_lwlocks[MAX_SIMUL_LWLOCKS];

我们用 NON_EXEC_STATIC LWLockPadded *LWLockArray 来描述系统所分配的lwlock对象在系统内存中的地址。

在函数 void CreateLWLocks(void)中,系统将对lwlock对象进行分配相应的内存空间并对所分配的lwlock对象进行初始化。 具体操作如下:

void CreateLWLocks (void)
{
ptr = (char *) ShmemAlloc(spaceLocks);
ptr += 2 * sizeof(int);
/* Ensure desired alignment of LWLock array */
ptr += LWLOCK_PADDED_SIZE – ((uintptr_t) ptr) % LWLOCK_PADDED_SIZE;
LWLockArray = (LWLockPadded *) ptr;
for (id = 0, lock = LWLockArray; id < numLocks; id++, lock++) //分配,初始化
{
SpinLockInit(&lock->lock.mutex);
lock->lock.releaseOK = true;
lock->lock.exclusive = 0;
lock->lock.shared = 0;
lock->lock.head = NULL;
lock->lock.tail = NULL;
}
LWLockCounter = (int *) ((char *) LWLockArray – 2 * sizeof(int));
LWLockCounter[0] = (int) NumFixedLWLocks;
LWLockCounter[1] = numLocks;
}

其中,有一点是我们需要特别注意的是下面三行代码,其具有非常重要的作用,我们将所分配的lwlock对象的前两个int地址中分别保存所期望的下一个ID和现有已分配的数量。

LWLockCounter = (int *) ((char *) LWLockArray – 2 * sizeof(int));
LWLockCounter[0] = (int) NumFixedLWLocks;
LWLockCounter[1] = numLocks;

其内存形式如下:
——————————————————————————————————————
NumFixedLWLocks | numLocks | lockID_1 | lockID_2 | …. | lockID_N | …
——————————————————————————————————————

当我们需要一个锁时候,我们首先会从LWLockArray 中获取一个可用的LockID然后将其分配,该操作由函数LWLockId LWLockAssign(void)来完成。其实现如下:

LWLockId LWLockAssign(void)
{
LWLockId result;
/* use volatile pointer to prevent code rearrangement */
volatile int *LWLockCounter;
LWLockCounter = (int *) ((char *) LWLockArray – 2 * sizeof(int));
//获取相应锁ID的首地址
SpinLockAcquire(ShmemLock);
if (LWLockCounter[0] >= LWLockCounter[1])
//判定是否有可用的ID
{
SpinLockRelease(ShmemLock);
elog(ERROR, “no more LWLockIds available”);
}
result = (LWLockId) (LWLockCounter[0]++);
//获取一个有效的ID
SpinLockRelease(ShmemLock);
return result;
}

在讨论完基本的操作后,我们将讨论lwlock的获取问题。 其由函数 void LWLockAcquire(LWLockId lockid, LWLockMode mode)来完成根据相应的条件获取一个lwlock,如果没有可用的lwlock则会导致系统sleep直到有可用的lwlock存在,而这也会导致cancel和die中断被屏蔽直到锁被释放。

对于LWLock的使用环境PostgreSQL 有如下的描述:

Since LWLocks are normally used to protect not-very-long sections of computation, a process needs to be able to acquire and release the same lock many times during a single CPU time slice, even in the presence of contention.

在函数执行时候,首先根据所请求锁的ID,从LWLockArray中获得所对于的锁对象, 由语句volatile LWLock *lock = &(LWLockArray[lockid].lock); 完成根据锁ID获取相应对象,然后将cancel 和die的的中断进行屏蔽;在完成上述操作后进入轮询部分,轮询是否具有可用的锁。 其具体的逻辑如下:

1) 首先,执行 SpinLockAcquire(&lock->mutex);来获得该琐上的mutex。

2)根据锁的类型做进一步的处理, 对于LW_EXCLUSIVE模式的锁,意味着该锁只能被持有一次,如果某个对象A持有该锁后,则另外一个对象B试图在申请获取该锁时候,则应该令对象B进行等待,直到其持有者-对象A使用完该锁并释放该锁,对象B方可获得对该锁的访问。

if (mode == LW_EXCLUSIVE) //如果锁的类型为LW_EXCLUSIVE模式则
{
if (lock->exclusive == 0 && lock->shared == 0)
{
lock->exclusive++;
mustwait = false;
}
else
mustwait = true;
}

当没有任何对象获得过该锁时候,则将 lock->exclusive设置为1, 表明该锁已经被使用,其它进程不能够在获得对该锁的访问, mustwait 用来描述是否需要进行轮询操作,当mustwait 为false时,表明已经获得想要的锁,可以退出轮询,否则继续进行轮询。当mode != LW_EXCLUSIVE 时,在lock->exclusive为非exclusive模式时,只需将该锁上的持有者的数量加 1并设置mustwait 以表明以成功获得对该锁的访问。
{
if (lock->exclusive == 0)
{
lock->shared++;
mustwait = false;
}
else
mustwait = true;
}

3)执行 SpinLockRelease(&lock->mutex); 释放mutext,同时将所获得的所锁的ID放入已获得链表中,以便后续进行查找。
SpinLockRelease(&lock->mutex);
held_lwlocks[num_held_lwlocks++] = lockid;

至此,我们分析完LWLock的获得过程。下面我们分别对函数 LWLockConditionalAcquire, LWLockAcquireOrWait 进行剖析。 从PostgreSQL中对于这两个函数描述分别为:

“LWLockConditionalAcquire – acquire a lightweight lock in the specified mode. If the lock is not available, return FALSE with no side-effects. If successful, cancel/die interrupts are held off until lock release.”

也就是说当我们使用LWLockConditionalAcquire 来申请一个锁时候,如果获得成功,则返回true并且将cancel和die中断进行屏蔽;否则,返回false。LWLockConditionalAcquire 实现上与 LWLockAcquire非常相似,只是没有了 LWLockAcquire函数中的轮询机制,只是进行一次的尝试,如果成功则获得所请求的锁并返回成功标志,否则返回失败标志。

对于LWLockAcquireOrWait函数PostgreSQL对其的描述如下:

“LWLockAcquireOrWait – Acquire lock, or wait until it’s free; The semantics of this function are a bit funky. If the lock is currently free, it is acquired in the given mode, and the function returns true. If the lock isn’t immediately free, the function waits until it is released nd returns false, but does not acquire the lock.”

从上文中的描述我们可以看出如果当前所请求锁是空闲的状态,则以所申请的模式将该锁分配并且函数返回true;否则,进行等待直到该锁被其持有者释放并且该函数返回false。该函数与上述函数有不同之处便是,即使当所申请的锁的持有者释放后,该该函数并不会获该锁,而是返回一个false。

下面我们就详细的分析一下该函数:该函数的原型为:bool LWLockAcquireOrWait(LWLockId lockid, LWLockMode mode),其中 lockid为所申请锁的ID,参数mode则表明申请所的模式:exclusive或者not exclusive.

1) 首先根据ID从LWLockArray中获得相应的LWLock对象, volatile LWLock *lock = &(LWLockArray[lockid].lock);

2)对cancel和die中断进行屏蔽,该项操作由宏 HOLD_INTERRUPTS(); 来完成对上述两个中断的屏蔽,直到该函数退出时候恢复这两个中断。

3)获得lock对象的mutex. SpinLockAcquire(&lock->mutex);

4)根据申请锁的类型更新 lock对象的状态。

if (mode == LW_EXCLUSIVE) // exclusive模式
{
//exclusive ==0表明该锁并没有被分配出去,shared ==0 其上所有共享该锁的对象为0
if (lock->exclusive == 0 && lock->shared == 0)
{
lock->exclusive++; //更新其状态为以分配
mustwait = false;
}
else
mustwait = true;
}
else //非exclusive模式
{
//当为非exclusive模式下,如果该锁没有被以exlusive模式分配,我们只需要对其 上所共享的对象数量进行加一,用以表明共享该锁的对象数量
if (lock->exclusive == 0)
{
lock->shared++; //增加共享该锁的对象数量
mustwait = false;
}
else
mustwait = true;
}

5) 判定是否需要执行等待操作。若mustwait 为false则表明用户已经获得其所申请的锁,此时系统可以返回用户,当mustwait 为true的情况下,表明该锁被其它对象所占用,此时需要进行等待直到所占用该锁的对象释放对该锁的占用,当系统在执行进行等待时候,将其等待的进程加入到锁的等待进程队列中。

if (mustwait)
{
proc->lwWaiting = true;
//设置进行的相关状态,进行等待。
proc->lwWaitMode = LW_WAIT_UNTIL_FREE;
proc->lwWaitLink = NULL;

if (lock->head == NULL) //将该进程加入到锁的等待队列中,链接到等待队列的末尾
lock->head = proc;
else
lock->tail->lwWaitLink = proc;
lock->tail = proc;
SpinLockRelease(&lock->mutex);
for (;;) //进行轮询其持有者已经释放对该锁的持有,若其持有者释放对该锁持有,则函数执行返回。
{
/* “false” means cannot accept cancel/die interrupt here. */
PGSemaphoreLock(&proc->sem, false);
//如果锁的持有者释放该锁后,proc->lwWaiting则该值为false,此时该函数等到该锁,此时函数应该退出并返回false。而 proc->lwWaiting则会在LWLockRelease函数中进行设置为false.
if (!proc->lwWaiting)
break;
extraWaits++;
} // End for
} else //mustwait 为false则获得该锁而返回true. 此处仅释放lock对象的mutex.
{
/* We are done updating shared state of the lock itself. */
SpinLockRelease(&lock->mutex);
}

6) 执行返回操作,若正确的获得所申请的锁对象,则将其加入其持有锁队列中并返回,否则返回false.

if (mustwait) //mustwait 为true
{
/* Failed to get lock, so release interrupt holdoff */
RESUME_INTERRUPTS();
}
else //为false情况,表明已经顺利的获得锁,此时应该将所获得的锁加入其持有锁的队列中。
{
/* Add lock to list of locks held by this backend */
held_lwlocks[num_held_lwlocks++] = lockid;
}
return !mustwait;

至此,我们对函数LWLockAcquireOrWait的分析完成。对于该函数的使用场景PG给出了如下的描述,供大家参考,在此不在多说。

“This is currently used for WALWriteLock: when a backend flushes the WAL, holding WALWriteLock, it can flush the commit records of many other backends as a side-effect. Those other backends need to wait until the flush finishes, but don’t need to acquire the lock anymore. They can just wake up, observe that their records have already been flushed, and return.”

在完成了LWlock的申请所有情况后,我们最后一个工作便是分析下锁的释放。在锁的持有者执行 LWLockRelease函数的调用后,将释放对该锁的持有。LWLockRelease函数的原型为: void LWLockRelease(LWLockId lockid) 其中lockid为所持有锁的ID。根据我们上述的加锁的分析我们可以大致得出:(1)对lock对象的exclusive,shared等状态信息更新;(2)对其等待队列上的进行进行处理 等大致的结论。下面我们就来详细的分析下LWLockRelease.
1) 根据所述锁的ID,由LWLockArray数组中获得 lock对象。

volatile LWLock *lock = &(LWLockArray[lockid].lock);

2)将其从锁持有队列中删除该该锁。

for (i = num_held_lwlocks; –i >= 0;) //由于该锁可能是刚刚加入的锁,因此这里使用了一个小技巧,从后往前查找。
{
if (lockid == held_lwlocks[i]) //查找出其所在的位置。
break;
}
num_held_lwlocks–; //更新持有锁的数量。
for (; i < num_held_lwlocks; i++) //执行删除操作。将该锁位置之后的锁,往前移动一位。
held_lwlocks[i] = held_lwlocks[i + 1];

3) 更新lock的状态,若该锁为exclusive模式,则将其复位,否则将共享对象的数量减一。

SpinLockAcquire(&lock->mutex);
/* Release my hold on lock */
if (lock->exclusive > 0) //表明其实exclusive模式
lock->exclusive–; //设置为0,若exclusive为1
else
{
Assert(lock->shared > 0);
lock->shared–; //将其共享该锁的对象数量减一。
}

4) 检查该锁上的等待队列中的进程,如果有需要唤醒的进程,则执行唤醒操作。

head = lock->head; //获得等待队列的首指针
if (head != NULL)
{
//判定是否可以执行释放操作,如果lock对象exclusive,shared 不为零则表明该锁仍然在使用。
if (lock->exclusive == 0 && lock->shared == 0 && lock->releaseOK)
{
bool releaseOK = true;
proc = head; //等待队列的首指针
//移动到等待队列到第一个lwWaitMode不为LW_WAIT_UNTIL_FREE的进程
while (proc->lwWaitMode == LW_WAIT_UNTIL_FREE && proc->lwWaitLink)
proc = proc->lwWaitLink;
//若等待队列想申请一个exclusive模式的锁,则将该进程单独唤醒;否则申请的共享模式的锁,此时应该将所有等待该锁的进程进行唤醒。
if (proc->lwWaitMode != LW_EXCLUSIVE) //为shared模式的锁,则唤醒队列上所有的
{
while (proc->lwWaitLink != NULL && proc->lwWaitLink->lwWaitMode != LW_EXCLUSIVE)
{
if (proc->lwWaitMode != LW_WAIT_UNTIL_FREE)
releaseOK = false;
proc = proc->lwWaitLink;
}
}
lock->head = proc->lwWaitLink; //将其从等待队列中移除
proc->lwWaitLink = NULL;
if (proc->lwWaitMode != LW_WAIT_UNTIL_FREE) //更新锁的标志
releaseOK = false;
lock->releaseOK = releaseOK;
}
else
{
/* lock is still held, can’t awaken anything */
head = NULL;
}
}

5) 通知等待队列上的所有相关的等待进程,其已经完成该锁的释放。

while (head != NULL)
{
LOG_LWDEBUG(“LWLockRelease”, lockid, “release waiter”);
proc = head;
head = proc->lwWaitLink;
proc->lwWaitLink = NULL;

proc->lwWaiting = false; //修改为false,表明该锁已经被释放,其它等待进程可以获得对该锁的申请,该参数在LWLockAcquireOrWait函数中被使用
PGSemaphoreUnlock(&proc->sem);
}

6) 恢复相关的中断并返回用户。

RESUME_INTERRUPTS();

至此,我们已经完成了对LWLock的分析。对于系统中的latch使用,将在另外的文档中进行描述。

在PostgreSQL中其中的全局变量MainLWLockArray中包含了系统中所使用到的各种类型的LWLock共42种类型。例如:有共享内存索引使用的 ShmemIndexLock,有Oid产生时候所使用的OidGenLock等等。 具体详细如下所示:

#define ShmemIndexLock (&MainLWLockArray[1].lock)
#define OidGenLock (&MainLWLockArray[2].lock)
#define XidGenLock (&MainLWLockArray[3].lock)
#define ProcArrayLock (&MainLWLockArray[4].lock)
#define SInvalReadLock (&MainLWLockArray[5].lock)
#define SInvalWriteLock (&MainLWLockArray[6].lock)
#define WALBufMappingLock (&MainLWLockArray[7].lock)
#define WALWriteLock (&MainLWLockArray[8].lock)
#define ControlFileLock (&MainLWLockArray[9].lock)
#define CheckpointLock (&MainLWLockArray[10].lock)
#define CLogControlLock (&MainLWLockArray[11].lock)
#define SubtransControlLock (&MainLWLockArray[12].lock)
#define MultiXactGenLock (&MainLWLockArray[13].lock)
#define MultiXactOffsetControlLock (&MainLWLockArray[14].lock)
#define MultiXactMemberControlLock (&MainLWLockArray[15].lock)
#define RelCacheInitLock (&MainLWLockArray[16].lock)
#define CheckpointerCommLock (&MainLWLockArray[17].lock)
#define TwoPhaseStateLock (&MainLWLockArray[18].lock)
#define TablespaceCreateLock (&MainLWLockArray[19].lock)
#define BtreeVacuumLock (&MainLWLockArray[20].lock)
#define AddinShmemInitLock (&MainLWLockArray[21].lock)
#define AutovacuumLock (&MainLWLockArray[22].lock)
#define AutovacuumScheduleLock (&MainLWLockArray[23].lock)
#define SyncScanLock (&MainLWLockArray[24].lock)
#define RelationMappingLock (&MainLWLockArray[25].lock)
#define AsyncCtlLock (&MainLWLockArray[26].lock)
#define AsyncQueueLock (&MainLWLockArray[27].lock)
#define SerializableXactHashLock (&MainLWLockArray[28].lock)
#define SerializableFinishedListLock (&MainLWLockArray[29].lock)
#define SerializablePredicateLockListLock (&MainLWLockArray[30].lock)
#define OldSerXidLock (&MainLWLockArray[31].lock)
#define SyncRepLock (&MainLWLockArray[32].lock)
#define BackgroundWorkerLock (&MainLWLockArray[33].lock)
#define DynamicSharedMemoryControlLock (&MainLWLockArray[34].lock)
#define AutoFileLock (&MainLWLockArray[35].lock)
#define ReplicationSlotAllocationLock (&MainLWLockArray[36].lock)
#define ReplicationSlotControlLock (&MainLWLockArray[37].lock)
#define CommitTsControlLock (&MainLWLockArray[38].lock)
#define CommitTsLock (&MainLWLockArray[39].lock)
#define ReplicationOriginLock (&MainLWLockArray[40].lock)
#define MultiXactTruncationLock (&MainLWLockArray[41].lock)
#define OldSnapshotTimeMapLock (&MainLWLockArray[42].lock)