PostgreSQL Locks Tables Fast
Most lock conflicts in PostgreSQL never reach the heavyweight lock table: a fast path in shared memory handles the common cases with a single atomic word, and the lock table itself is partitioned into sixteen independently-locked hash partitions.
Ask PostgreSQL how many locks it takes and the honest answer is “it depends,
and the interesting part is which lock you’re talking about”. The
src/backend/storage/lmgr/README file opens with four kinds of interprocess
lock — spinlocks, lightweight locks, regular (heavyweight) locks, and
predicate locks — and the heavyweight one is itself split in two: a shared
hash table, and a per-backend fast path.
Every SELECT on a table takes an AccessShareLock. Every INSERT takes a
RowExclusiveLock. Every ALTER TABLE takes an AccessExclusiveLock and
then waits for every other session to go away. If all of that went through
one lock table protected by one mutex, a thousand concurrent readers would
be a thousand processes fighting over one cache line, and the lock manager
would be the thing that limits your database’s concurrency.
It isn’t, because most of those locks never touch the lock table at all.
There are really two managers here:
| Heavyweight lock manager | Fast path | |
|---|---|---|
| Where it lives | shared hash table | per-backend slice of PGPROC |
| Protected by | an LWLock, 16-way partitioned | proc->fpInfoLock |
| Lock modes | all 8 | 3 (weak relation locks) |
| Object types | everything | relations in the current database |
| Deadlock detection | yes | no (but also no waiting) |
| Cost | hash lookup + LWLock | a few instructions |
The fast path exists for the overwhelmingly common case: a lock that nobody
will ever wait for. If you already hold AccessShareLock on a table and you
ask for AccessShareLock again, or if you take RowExclusiveLock on a
table nobody is altering, you do not need a heavyweight lock. You need a
counter.
This is a walk through how it all works, based on reading the PostgreSQL
source (version 20devel, commit e75fcc9867f).
The lock tag: sixteen bytes that name anything
Everything in the lock manager is keyed by a LOCKTAG. It lives in
src/include/storage/locktag.h (it used to live in lock.h, and was split
out), and it is designed with malice aforethought:
/*
* The LOCKTAG struct is defined with malice aforethought to fit into 16
* bytes with no padding. Note that this would need adjustment if we were
* to widen Oid, BlockNumber, or TransactionId to more than 32 bits.
*
* We include lockmethodid in the locktag so that a single hash table in
* shared memory can store locks of different lockmethods.
*/
typedef struct LOCKTAG
{
uint32 locktag_field1; /* a 32-bit ID field */
uint32 locktag_field2; /* a 32-bit ID field */
uint32 locktag_field3; /* a 32-bit ID field */
uint16 locktag_field4; /* a 16-bit ID field */
uint8 locktag_type; /* see enum LockTagType */
uint8 locktag_lockmethodid; /* lockmethod indicator */
} LOCKTAG;Four bytes, four bytes, four bytes, two bytes, one byte, one byte: 16 bytes,
no padding. The “one field per byte” framing in the field names is
deliberately generic — the struct doesn’t know what it’s identifying, only
how many bits each component needs. The interpretation is fixed by
locktag_type:
typedef enum LockTagType
{
LOCKTAG_RELATION, /* whole relation */
LOCKTAG_RELATION_EXTEND, /* the right to extend a relation */
LOCKTAG_DATABASE_FROZEN_IDS, /* pg_database.datfrozenxid */
LOCKTAG_PAGE, /* one page of a relation */
LOCKTAG_TUPLE, /* one physical tuple */
LOCKTAG_TRANSACTION, /* transaction (for waiting for xact done) */
LOCKTAG_VIRTUALTRANSACTION, /* virtual transaction (ditto) */
LOCKTAG_SPECULATIVE_TOKEN, /* speculative insertion Xid and token */
LOCKTAG_OBJECT, /* non-relation database object */
LOCKTAG_USERLOCK, /* reserved for old contrib/userlock code */
LOCKTAG_ADVISORY, /* advisory user locks */
LOCKTAG_APPLY_TRANSACTION, /* transaction being applied on a logical
* replication subscriber */
} LockTagType;Twelve kinds of lockable object. LOCKTAG_RELATION is the one you care
about most of the time; LOCKTAG_TUPLE is the one that sounds scary but
isn’t — tuple locks are a serialization fiction (the “locked tuple” flag on
the tuple header), not entries in the lock table.
The comment about lockmethodid is the other half of the design. There are
two lock methods — DEFAULT_LOCKMETHOD and USER_LOCKMETHOD — and they
have identical mode semantics today but are conceptually separate. By
putting the method id in the tag, one shared hash table can hold both, and
the code can look up the right semantics table from the tag alone.
Filling in the tag
Because the fields are generic, there are macros for every kind of tag:
/* ID info for a relation is DB OID + REL OID; DB OID = 0 if shared */
#define SET_LOCKTAG_RELATION(locktag,dboid,reloid) \
((locktag).locktag_field1 = (dboid), \
(locktag).locktag_field2 = (reloid), \
(locktag).locktag_field3 = 0, \
(locktag).locktag_field4 = 0, \
(locktag).locktag_type = LOCKTAG_RELATION, \
(locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD)Note the first field: the database OID is part of every relation tag.
That is what makes a lock on mytable in db1 and a lock on mytable in
db2 different locks, without any special-casing in the hash table.
SET_LOCKTAG_ADVISORY() shows the other pattern — the fields are just
whatever the caller wants them to be:
#define SET_LOCKTAG_ADVISORY(locktag,id1,id2,id3,id4) \
((locktag).locktag_field1 = (id1), \
(locktag).locktag_field2 = (id2), \
(locktag).locktag_field3 = (id3), \
(locktag).locktag_field4 = (id4), \
(locktag).locktag_type = LOCKTAG_ADVISORY, \
(locktag).locktag_lockmethodid = USER_LOCKMETHOD)and src/backend/utils/adt/lockfuncs.c uses it for pg_advisory_lock(),
with a comment describing the packing:
/*
* Functions for manipulating advisory locks
*
* We make use of the locktag fields as follows:
*
* field1: MyDatabaseId ... ensures locks are local to each database
* field2: first of 2 int4 keys, or high-order half of an int8 key
* field3: second of 2 int4 keys, or low-order half of an int8 key
* field4: 1 if using an int8 key, 2 if using 2 int4 keys
*/
#define SET_LOCKTAG_INT64(tag, key64) \
SET_LOCKTAG_ADVISORY(tag, \
MyDatabaseId, \
(uint32) ((key64) >> 32), \
(uint32) (key64), \
1)This is the SQL surface of the lock manager. lockfuncs.c is a thin layer:
pg_lock_status() (the pg_locks view), pg_blocking_pids(), and the
advisory-lock functions. The machinery itself is all in lock.c.
The heavyweight lock table
The shared lock table is two hash tables, both partitioned.
The first is keyed by LOCKTAG and holds one LOCK struct per lockable
object that currently has locks on it (src/include/storage/lock.h):
typedef struct LOCK
{
/* hash key */
LOCKTAG tag; /* unique identifier of lockable object */
/* data */
LOCKMASK grantMask; /* bitmask for lock types already granted */
LOCKMASK waitMask; /* bitmask for lock types awaited */
dlist_head procLocks; /* list of PROCLOCK objects assoc. with lock */
dclist_head waitProcs; /* list of PGPROC objects waiting on lock */
int requested[MAX_LOCKMODES]; /* counts of requested locks */
int nRequested; /* total of requested[] array */
int granted[MAX_LOCKMODES]; /* counts of granted locks */
int nGranted; /* total of granted locks */
} LOCK;The LOCK object is the aggregation of all holders and waiters for one
object. grantMask is a bitmask of which modes are held by somebody;
waitMask is which modes somebody is waiting for. Those two bitmasks are
what make the common case cheap: a conflict check is
conflictTab[lockmode] & lock->grantMask, one AND.
The second table is keyed by PROCLOCKTAG — the pair (LOCK, PGPROC) — and
holds the per-holder detail:
typedef struct PROCLOCKTAG
{
/* NB: we assume this struct contains no padding! */
LOCK *myLock; /* link to per-lockable-object information */
PGPROC *myProc; /* link to PGPROC of owning backend */
} PROCLOCKTAG;
typedef struct PROCLOCK
{
/* tag */
PROCLOCKTAG tag; /* unique identifier of proclock object */
/* data */
PGPROC *groupLeader; /* proc's lock group leader, or proc itself */
LOCKMASK holdMask; /* bitmask for lock types currently held */
LOCKMASK releaseMask; /* bitmask for lock types to be released */
dlist_node lockLink; /* list link in LOCK's list of proclocks */
dlist_node procLink; /* list link in PGPROC's list of proclocks */
} PROCLOCK;So: one LOCK per object, one PROCLOCK per (object, backend). A table
locked by fifty sessions has one LOCK and fifty PROCLOCKs.
Partitioning
The two hash tables are created with a partition count:
max_table_size = NLOCKENTS();
/*
* Hash table for LOCK structs. This stores per-locked-object
* information.
*/
ShmemRequestHash(.name = "LOCK hash",
.nelems = max_table_size,
.ptr = &LockMethodLockHash,
.hash_info.keysize = sizeof(LOCKTAG),
.hash_info.entrysize = sizeof(LOCK),
.hash_info.num_partitions = NUM_LOCK_PARTITIONS,
.hash_flags = HASH_ELEM | HASH_BLOBS | HASH_PARTITION,
);
/* Assume an average of 2 holders per lock */
max_table_size *= 2;
ShmemRequestHash(.name = "PROCLOCK hash",
.nelems = max_table_size,
.ptr = &LockMethodProcLockHash,
.hash_info.keysize = sizeof(PROCLOCKTAG),
.hash_info.entrysize = sizeof(PROCLOCK),
.hash_info.hash = proclock_hash,
.hash_info.num_partitions = NUM_LOCK_PARTITIONS,
.hash_flags = HASH_ELEM | HASH_FUNCTION | HASH_PARTITION,
);and NUM_LOCK_PARTITIONS is 16:
/* Number of partitions the shared lock tables are divided into */
#define LOG2_NUM_LOCK_PARTITIONS 4
#define NUM_LOCK_PARTITIONS (1 << LOG2_NUM_LOCK_PARTITIONS)Each partition has its own LWLock, and the partition is chosen by the hash code:
/*
* The lockmgr's shared hash tables are partitioned to reduce contention.
* To determine which partition a given locktag belongs to, compute the tag's
* hash code with LockTagHashCode(), then apply one of these macros.
* NB: NUM_LOCK_PARTITIONS must be a power of 2!
*/
#define LockHashPartition(hashcode) \
((hashcode) % NUM_LOCK_PARTITIONS)
#define LockHashPartitionLock(hashcode) \
(&MainLWLockArray[LOCK_MANAGER_LWLOCK_OFFSET + \
LockHashPartition(hashcode)].lock)So: lock contention is spread over sixteen independent LWLocks. Two sessions locking unrelated tables have a 15-in-16 chance of not contending on the partition lock at all. This is the mechanism behind “PostgreSQL scales well to many concurrent sessions” for lock-heavy workloads.
The hash code itself comes from the hash table’s own hash function:
/*
* Compute the hash code associated with a LOCKTAG.
*
* To avoid unnecessary recomputations of the hash code, we try to do this
* just once per function, and then pass it around as needed. Aside from
* passing the hashcode to hash_search_with_hash_value(), we can extract
* the lock partition number from the hashcode.
*/
uint32
LockTagHashCode(const LOCKTAG *locktag)
{
return get_hash_value(LockMethodLockHash, locktag);
}The PROCLOCK table uses a different hash function, proclock_hash(), and
the reason is a lovely bit of engineering: the proclock table must use the
same partition locks as the LOCK table.
/*
* Compute the hash code associated with a PROCLOCKTAG.
*
* Because we want to use just one set of partition locks for both the
* LOCK and PROCLOCK hash tables, we have to make sure that PROCLOCKs
* fall into the same partition number as their associated LOCKs.
*/Both tables are looked up while holding a single partition lock, so one
LWLockAcquire covers both.
The per-backend local lock table
Every backend also keeps a local, non-shared hash table of the locks it
holds. src/include/storage/lock.h explains why:
/*
* Each backend also maintains a local hash table with information about each
* lock it is currently interested in. In particular the local table counts
* the number of times that lock has been acquired. This allows multiple
* requests for the same lock to be executed without additional accesses to
* shared memory. We also track the number of lock acquisitions per
* ResourceOwner, so that we can release just those locks belonging to a
* particular ResourceOwner.
*
* When holding a lock taken "normally", the lock and proclock fields always
* point to the associated objects in shared memory. However, if we acquired
* the lock via the fast-path mechanism, the lock and proclock fields are set
* to NULL, since there probably aren't any such objects in shared memory.
* (If the lock later gets promoted to normal representation, we may eventually
* update our locallock's lock/proclock fields after finding the shared
* objects.)
*
* Caution: a locallock object can be left over from a failed lock acquisition
* attempt. In this case its lock/proclock fields are untrustworthy, since
* the shared lock object is neither held nor awaited, and hence is available
* to be reclaimed. If nLocks > 0 then these pointers must either be valid or
* NULL, but when nLocks == 0 they should be considered garbage.
*/The struct:
typedef struct LOCALLOCK
{
/* tag */
LOCALLOCKTAG tag; /* unique identifier of locallock entry */
/* data */
uint32 hashcode; /* copy of LOCKTAG's hash value */
LOCK *lock; /* associated LOCK object, if any */
PROCLOCK *proclock; /* associated PROCLOCK object, if any */
int64 nLocks; /* total number of times lock is held */
int numLockOwners; /* # of relevant ResourceOwners */
int maxLockOwners; /* allocated size of array */
LOCALLOCKOWNER *lockOwners; /* dynamically resizable array */
bool holdsStrongLockCount; /* bumped FastPathStrongRelationLocks */
bool lockCleared; /* we read all sinval msgs for lock */
} LOCALLOCK;Note that the local table is keyed by tag and mode:
typedef struct LOCALLOCKTAG
{
LOCKTAG lock; /* identifies the lockable object */
LOCKMODE mode; /* lock mode for this table entry */
} LOCALLOCKTAG;which is how a backend can hold AccessShareLock and RowExclusiveLock on
the same table as two separate entries with separate counts.
It’s created in InitLockManagerAccess():
info.keysize = sizeof(LOCALLOCKTAG);
info.entrysize = sizeof(LOCALLOCK);
LockMethodLocalHash = hash_create("LOCALLOCK hash",
16,
&info,
HASH_ELEM | HASH_BLOBS);The payoff is in LockAcquireExtended(): the very first thing it does
after finding the locallock is check whether the lock is already held, and
if so, bump a counter and return without touching shared memory at all:
/*
* If we already hold the lock, we can just increase the count locally.
*
* If lockCleared is already set, caller need not worry about absorbing
* sinval messages related to the lock's object.
*/
if (locallock->nLocks > 0)
{
GrantLockLocal(locallock, owner);
if (locallock->lockCleared)
return LOCKACQUIRE_ALREADY_CLEAR;
else
return LOCKACQUIRE_ALREADY_HELD;
}This is the single most important optimization in the whole lock manager for
read-heavy workloads. A query that opens a cursor and reads a thousand
rows takes AccessShareLock once, but the executor re-acquires the relation
lock for every row it fetches in some code paths; with the local table,
each re-acquisition is a local hash lookup and an integer increment.
The fast path
Now the fun part.
Where fast-path locks live
Fast-path locks are not in the lock table at all. They live in the
backend’s own PGPROC struct, in shared memory, in two arrays:
/*-- recording fast-path locks taken by this backend. --*/
LWLock fpInfoLock; /* protects per-backend fast-path state */
uint64 *fpLockBits; /* lock modes held for each fast-path slot */
Oid *fpRelId; /* slots for rel oids */fpRelId[f] is the relation OID in slot f; fpLockBits[f] is a bitmask
of which lock modes are held on it. The arrays are indexed by group and
slot:
#define FP_LOCK_GROUPS_PER_BACKEND_MAX 1024
#define FP_LOCK_SLOTS_PER_GROUP 16 /* don't change */
#define FastPathLockSlotsPerBackend() \
(FP_LOCK_SLOTS_PER_GROUP * FastPathLockGroupsPerBackend)and the group for a relation is a cheap hash:
/*
* Macros to calculate the fast-path group and index for a relation.
*
* The formula is a simple hash function, designed to spread the OIDs a bit,
* so that even contiguous values end up in different groups. In most cases
* there will be gaps anyway, but the multiplication should help a bit.
*
* The selected constant (49157) is a prime not too close to 2^k, and it's
* small enough to not cause overflows (in 64-bit).
*
* We can assume that FastPathLockGroupsPerBackend is a power-of-two per
* InitializeFastPathLocks().
*/
#define FAST_PATH_REL_GROUP(rel) \
(((uint64) (rel) * 49157) & (FastPathLockGroupsPerBackend - 1))So with the default configuration there are 8 groups of 16 slots each: 128 fast-path relation slots per backend, any of which can hold any of the three weak lock modes.
The bit packing is dense. Each slot uses 3 bits of the 64-bit word, and there are 3 usable lock modes (plus the offset):
/* Macros for manipulating proc->fpLockBits */
#define FAST_PATH_BITS_PER_SLOT 3
#define FAST_PATH_LOCKNUMBER_OFFSET 1
#define FAST_PATH_MASK ((1 << FAST_PATH_BITS_PER_SLOT) - 1)Which locks are eligible
/*
* The fast-path lock mechanism is concerned only with relation locks on
* unshared relations by backends bound to a database. The fast-path
* mechanism exists mostly to accelerate acquisition and release of locks
* that rarely conflict. Because ShareUpdateExclusiveLock is
* self-conflicting, it can't use the fast-path mechanism; but it also does
* not conflict with any of the locks that do, so we can ignore it completely.
*/
#define EligibleForRelationFastPath(locktag, mode) \
((locktag)->locktag_lockmethodid == DEFAULT_LOCKMETHOD && \
(locktag)->locktag_type == LOCKTAG_RELATION && \
(locktag)->locktag_field1 == MyDatabaseId && \
MyDatabaseId != InvalidOid && \
(mode) < ShareUpdateExclusiveLock)
#define ConflictsWithRelationFastPath(locktag, mode) \
((locktag)->locktag_lockmethodid == DEFAULT_LOCKMETHOD && \
(locktag)->locktag_type == LOCKTAG_RELATION && \
(locktag)->locktag_field1 != InvalidOid && \
(mode) > ShareUpdateExclusiveLock)Read the second macro carefully: it fires for modes strictly greater than
ShareUpdateExclusiveLock, i.e. ShareLock, ShareRowExclusiveLock,
ExclusiveLock, AccessExclusiveLock. Those are the modes that conflict
with the fast-path modes, and so they are the modes that must disable the
fast path.
ShareUpdateExclusiveLock itself is excluded from both: it’s
self-conflicting, so it can’t go on the fast path, but it doesn’t conflict
with anything that can, so it doesn’t have to disable it either. Neat.
Note also that the fast path requires locktag_field1 == MyDatabaseId —
locks on shared relations (locktag_field1 == InvalidOid, e.g.
pg_class catalogs) are never eligible, because they’re visible across
databases and a fast-path slot in one backend’s PGPROC says nothing about
another database’s sessions.
Granting a fast-path lock
The grant path is in LockAcquireExtended():
if (EligibleForRelationFastPath(locktag, lockmode))
{
if (FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] <
FP_LOCK_SLOTS_PER_GROUP)
{
uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
bool acquired;
/*
* LWLockAcquire acts as a memory sequencing point, so it's safe
* to assume that any strong locker whose increment to
* FastPathStrongRelationLocks becomes visible after we test it
* has yet to begin to transfer fast-path locks.
*/
LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
if (pg_atomic_read_u32(&FastPathStrongRelationLocks[fasthashcode]) != 0)
acquired = false;
else
acquired = FastPathGrantRelationLock(locktag->locktag_field2,
lockmode);
LWLockRelease(&MyProc->fpInfoLock);
if (acquired)
{
/*
* The locallock might contain stale pointers to some old
* shared objects; we MUST reset these to null before
* considering the lock to be acquired via fast-path.
*/
locallock->lock = NULL;
locallock->proclock = NULL;
GrantLockLocal(locallock, owner);
return LOCKACQUIRE_OK;
}
}
else
{
/*
* Increment the lock statistics counter if lock could not be
* acquired via the fast-path.
*/
pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type);
}
}Notice what was not taken: the partition LWLock. The only lock acquired is
MyProc->fpInfoLock, which protects this backend’s own fast-path state.
Nobody else touches those slots — unless someone requests a strong lock, at
which point they have to come and get them.
FastPathGrantRelationLock() is the actual slot allocation:
static bool
FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
{
uint32 i;
uint32 unused_slot = FastPathLockSlotsPerBackend();
/* fast-path group the lock belongs to */
uint32 group = FAST_PATH_REL_GROUP(relid);
/* Scan for existing entry for this relid, remembering empty slot. */
for (i = 0; i < FP_LOCK_SLOTS_PER_GROUP; i++)
{
/* index into the whole per-backend array */
uint32 f = FAST_PATH_SLOT(group, i);
if (FAST_PATH_GET_BITS(MyProc, f) == 0)
unused_slot = f;
else if (MyProc->fpRelId[f] == relid)
{
Assert(!FAST_PATH_CHECK_LOCKMODE(MyProc, f, lockmode));
FAST_PATH_SET_LOCKMODE(MyProc, f, lockmode);
return true;
}
}
/* If no existing entry, use any empty slot. */
if (unused_slot < FastPathLockSlotsPerBackend())
{
MyProc->fpRelId[unused_slot] = relid;
FAST_PATH_SET_LOCKMODE(MyProc, unused_slot, lockmode);
++FastPathLocalUseCounts[group];
return true;
}
/* No existing entry, and no empty slot. */
return false;
}A scan of 16 slots within one group, and — crucially — a scan of one group, not of the whole array. The hash function is what keeps this O(1): you only look at the 16 slots that could possibly hold this relation. This is a very different shape from the heavyweight path, where you must take a partition lock and do a hash table lookup.
Note that FastPathGrantRelationLock returning false (no free slot) is not
an error: the caller falls through to the heavyweight path. The fast path
is an optimization, and running out of slots just means you take the slow
path.
Releasing a fast-path lock
Symmetrically, in LockRelease():
/* Attempt fast release of any lock eligible for the fast path. */
if (EligibleForRelationFastPath(locktag, lockmode) &&
FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] > 0)
{
bool released;
/*
* We might not find the lock here, even if we originally entered it
* here. Another backend may have moved it to the main table.
*/
LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
released = FastPathUnGrantRelationLock(locktag->locktag_field2,
lockmode);
LWLockRelease(&MyProc->fpInfoLock);
if (released)
{
RemoveLocalLock(locallock);
return true;
}
}That comment — “Another backend may have moved it to the main table” — is the whole story of the next section.
Upgrading from fast path to heavyweight
Here is the subtle part. Suppose session A holds a fast-path
AccessShareLock on t, and session B wants AccessExclusiveLock on
t. B’s request conflicts with the fast-path mode, so B must:
- disable the fast path for that relation, so nobody takes a fast-path lock that B can’t see; and
- transfer any existing fast-path locks on that relation into the heavyweight table, so that B’s conflict check can see them.
Step 1 is a counter array:
/*
* To make the fast-path lock mechanism work, we must have some way of
* preventing the use of the fast-path when a conflicting lock might be
* present. We partition* the locktag space into FAST_PATH_STRONG_LOCK_HASH_PARTITIONS,
* and maintain an integer count of the number of "strong" lockers
* in each partition. When any "strong" lockers are present (which is
* hopefully not very often), the fast-path mechanism can't be used, and we
* must fall back to the slower method of pushing matching locks directly
* into the main lock tables.
*
* The deadlock detector does not know anything about the fast path mechanism,
* so any locks that might be involved in a deadlock must be transferred from
* the fast-path queues to the main lock table.
*/
#define FAST_PATH_STRONG_LOCK_HASH_BITS 10
#define FAST_PATH_STRONG_LOCK_HASH_PARTITIONS \
(1 << FAST_PATH_STRONG_LOCK_HASH_BITS)
#define FastPathStrongLockHashPartition(hashcode) \
((hashcode) % FAST_PATH_STRONG_LOCK_HASH_PARTITIONS)
static pg_atomic_uint32 *FastPathStrongRelationLocks;1024 partitions of a counter array. The counter is checked by every fast-path acquisition, and it is the mechanism that makes the whole thing safe.
Step 2 is FastPathTransferRelationLocks(), which walks every backend:
static bool
FastPathTransferRelationLocks(LockMethod lockMethodTable, const LOCKTAG *locktag,
uint32 hashcode)
{
LWLock *partitionLock = LockHashPartitionLock(hashcode);
Oid relid = locktag->locktag_field2;
uint32 i;
/* fast-path group the lock belongs to */
uint32 group = FAST_PATH_REL_GROUP(relid);
/*
* Every PGPROC that can potentially hold a fast-path lock is present in
* ProcGlobal->allProcs. Prepared transactions are not, but any
* outstanding fast-path locks held by prepared transactions are
* transferred to the main lock table.
*/
for (i = 0; i < ProcGlobal->allProcCount; i++)
{
PGPROC *proc = GetPGProcByNumber(i);
uint32 j;
LWLockAcquire(&proc->fpInfoLock, LW_EXCLUSIVE);
...
for (j = 0; j < FP_LOCK_SLOTS_PER_GROUP; j++)
{
uint32 lockmode;
/* index into the whole per-backend array */
uint32 f = FAST_PATH_SLOT(group, j);
/* Look for an allocated slot matching the given relid. */
if (relid != proc->fpRelId[f] || FAST_PATH_GET_BITS(proc, f) == 0)
continue;
/* Find or create lock object. */
LWLockAcquire(partitionLock, LW_EXCLUSIVE);
for (lockmode = FAST_PATH_LOCKNUMBER_OFFSET;
lockmode < FAST_PATH_LOCKNUMBER_OFFSET + FAST_PATH_BITS_PER_SLOT;
++lockmode)
{
PROCLOCK *proclock;
if (!FAST_PATH_CHECK_LOCKMODE(proc, f, lockmode))
continue;
proclock = SetupLockInTable(lockMethodTable, proc, locktag,
hashcode, lockmode);
...
GrantLock(proclock->tag.myLock, proclock, lockmode);
FAST_PATH_CLEAR_LOCKMODE(proc, f, lockmode);
}
LWLockRelease(partitionLock);
...
}
LWLockRelease(&proc->fpInfoLock);
}
return true;
}Yes: to take a strong lock on a table, you walk every backend in the cluster. That is O(MaxBackends), and it is why the fast path is a bet that strong locks are rare.
But notice the early exit:
/*
* If the target backend isn't referencing the same database as the
* lock, then we needn't examine the individual relation IDs at all;
* none of them can be relevant.
*
* proc->databaseId is set at backend startup time and never changes
* thereafter, so it might be safe to perform this test before
* acquiring &proc->fpInfoLock. ...
*
* Also skip groups without any registered fast-path locks.
*/
if (proc->databaseId != locktag->locktag_field1 ||
proc->fpLockBits[group] == 0)
{
LWLockRelease(&proc->fpInfoLock);
continue;
}Two skips: backends in other databases (their locks can never be on this relation), and backends with nothing in this group. In practice the second skip kills almost all of the work.
And when the transferring backend is the same one that holds the fast-path
lock, there’s a third path. If you hold a fast-path AccessShareLock and
then ask for RowExclusiveLock on the same table, your new request is not
fast-path eligible (well, RowExclusiveLock is weak, so it would take
another fast-path slot)… but if you ask for AccessExclusiveLock, your
own fast-path lock has to be visible to the heavyweight machinery, and the
transfer loop handles that — it walks all backends including you.
The strong-lock counter
The FastPathStrongRelationLocks array is incremented by
BeginStrongLockAcquire():
/*
* BeginStrongLockAcquire - inhibit use of fastpath for a given LOCALLOCK,
* and arrange for error cleanup if it fails
*/
static void
BeginStrongLockAcquire(LOCALLOCK *locallock, uint32 fasthashcode)
{
Assert(StrongLockInProgress == NULL);
Assert(locallock->holdsStrongLockCount == false);
pg_atomic_fetch_add_u32(&FastPathStrongRelationLocks[fasthashcode], 1);
locallock->holdsStrongLockCount = true;
StrongLockInProgress = locallock;
}and — importantly — it is not decremented when the acquisition finishes.
FinishStrongLockAcquire() only clears the cleanup hook:
/*
* FinishStrongLockAcquire - cancel pending cleanup for a strong lock
* acquisition once it's no longer needed
*/
static void
FinishStrongLockAcquire(void)
{
StrongLockInProgress = NULL;
}The decrement happens when the locallock entry is finally removed — that is, when the strong lock is released:
if (locallock->holdsStrongLockCount)
{
uint32 fasthashcode;
fasthashcode = FastPathStrongLockHashPartition(locallock->hashcode);
Assert(pg_atomic_read_u32(&FastPathStrongRelationLocks[fasthashcode]) > 0);
pg_atomic_fetch_sub_u32(&FastPathStrongRelationLocks[fasthashcode], 1);
locallock->holdsStrongLockCount = false;
}So the semantics are: while a strong lock is held on a relation, the fast path is disabled for that hash partition. That is exactly right — a strong lock conflicts with the weak modes, so while it is outstanding, nobody may take a weak lock that the strong locker would fail to see. Every fast-path acquisition in that partition now fails the atomic check and falls back to the heavyweight table.
This is a partitioned disable, not a global one. A strong lock on one table only disables the fast path for the hash partition that table falls into.
Conflicts
The conflict table
Every lock method has a table of which modes conflict with which:
static const LOCKMASK LockConflicts[] = {
0,
/* AccessShareLock */
LOCKBIT_ON(AccessExclusiveLock),
/* RowShareLock */
LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock),
/* RowExclusiveLock */
LOCKBIT_ON(ShareLock) | LOCKBIT_ON(ShareRowExclusiveLock) |
LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock),
...and the check is one AND against the aggregate mask:
/*
* first check for global conflicts: If no locks conflict with my request,
* then I get the lock.
*
* Checking for conflict: lock->grantMask represents the types of
* currently held locks. conflictTable[lockmode] has a bit set for each
* type of lock that conflicts with request. Bitwise compare tells if
* there is a conflict.
*/
if (!(conflictMask & lock->grantMask))
{
PROCLOCK_PRINT("LockCheckConflicts: no conflict", proclock);
return false;
}If that’s non-zero, there’s potentially a conflict, and the code has to look at individual proclocks, because some of the conflicting locks might be held by me:
/*
* Rats. Something conflicts. But it could still be my own lock, or a
* lock held by another member of my locking group. First, figure out how
* many conflicts remain after subtracting out any locks I hold myself.
*/
myLocks = proclock->holdMask;
for (i = 1; i <= numLockModes; i++)
{
if ((conflictMask & LOCKBIT_ON(i)) == 0)
{
conflictsRemaining[i] = 0;
continue;
}
conflictsRemaining[i] = lock->granted[i];
if (myLocks & LOCKBIT_ON(i))
--conflictsRemaining[i];
totalConflictsRemaining += conflictsRemaining[i];
}
/* If no conflicts remain, we get the lock. */
if (totalConflictsRemaining == 0)
{
PROCLOCK_PRINT("LockCheckConflicts: resolved (simple)", proclock);
return false;
}The granted[] array — a count per mode — is what makes this work without
walking the proclock list. You know how many AccessExclusiveLocks are
held; you subtract the ones you hold yourself; if the count is zero for
every conflicting mode, you can have the lock.
Lock groups
If group locking is in play, you also have to subtract the locks held by other members of your group:
/*
* Locks held in conflicting modes by members of our own lock group are
* not real conflicts; we can subtract those out and see if we still have
* a conflict. This is O(N) in the number of processes holding or
* awaiting locks on this object. We could improve that by making the
* shared memory state more complex (and larger) but it doesn't seem worth
* it.
*/
dlist_foreach(proclock_iter, &lock->procLocks)
{
PROCLOCK *otherproclock =
dlist_container(PROCLOCK, lockLink, proclock_iter.cur);
if (proclock != otherproclock &&
proclock->groupLeader == otherproclock->groupLeader &&
(otherproclock->holdMask & conflictMask) != 0)
{
...
}
}The groupLeader field on PROCLOCK is computed once, at creation:
/*
* It might seem unsafe to access proclock->groupLeader without a
* lock, but it's not really. Either we are initializing a proclock
* on our own behalf, in which case our group leader isn't changing
* because the group leader for a process can only ever be changed by
* the process itself; or else we are transferring a fast-path lock to
* the main lock table, in which case that process can't change its
* lock group leader without first releasing all of its locks (and in
* particular the one we are currently transferring).
*/
proclock->groupLeader = proc->lockGroupLeader != NULL ?
proc->lockGroupLeader : proc;A group is a leader PGPROC plus members. BecomeLockGroupLeader() creates
one:
/*
* BecomeLockGroupLeader - designate process as lock group leader
*
* Once this function has returned, other processes can join the lock group
* by calling BecomeLockGroupMember.
*/
void
BecomeLockGroupLeader(void)
{
LWLock *leader_lwlock;
/* If we already did it, we don't need to do it again. */
if (MyProc->lockGroupLeader == MyProc)
return;
/* We had better not be a follower. */
Assert(MyProc->lockGroupLeader == NULL);
/* Create single-member group, containing only ourselves. */
leader_lwlock = LockHashPartitionLockByProc(MyProc);
LWLockAcquire(leader_lwlock, LW_EXCLUSIVE);
MyProc->lockGroupLeader = MyProc;
dlist_push_head(&MyProc->lockGroupMembers, &MyProc->lockGroupLink);
LWLockRelease(leader_lwlock);
}The group leader’s own partition lock protects the group’s membership fields:
/*
* The deadlock detector needs to be able to access lockGroupLeader and
* related fields in the PGPROC, so we arrange for those fields to be protected
* by one of the lock hash partition locks. Since the deadlock detector
* acquires all such locks anyway, this makes it safe for it to access these
* fields without doing anything extra. To avoid contention as much as
* possible, we map different PGPROCs to different partition locks. The lock
* used for a given lock group is determined by the group leader's pgprocno.
*/
#define LockHashPartitionLockByProc(leader_pgproc) \
LockHashPartitionLock(GetNumberFromPGProc(leader_pgproc))Groups exist so that a set of processes can hold conflicting locks on the
same object without deadlocking each other — used by parallel query
workers and by REPACK. The point is that a group is treated as one
participant in the conflict and deadlock algorithms.
GetLockConflicts
Some callers (logical replication conflict resolution, for instance) need
to know who is blocking them, not just that someone is. That’s
GetLockConflicts():
/*
* GetLockConflicts
* Get an array of VirtualTransactionIds of xacts currently holding locks
* that would conflict with the specified lock/lockmode.
* xacts merely awaiting such a lock are NOT reported.
*
* The result array is palloc'd and is terminated with an invalid VXID.
* *countp, if not null, is updated to the number of items set.
*
* Of course, the result could be out of date by the time it's returned, so
* use of this function has to be thought about carefully. Similarly, a
* PGPROC with no "lxid" will be considered non-conflicting regardless of any
* lock it holds. ...
*/The interesting part is that it has to check both the heavyweight table and every backend’s fast-path array:
/*
* Fast path locks might not have been entered in the primary lock table.
* If the lock we're dealing with could conflict with such a lock, we must
* examine each backend's fast-path array for conflicts.
*/
if (ConflictsWithRelationFastPath(locktag, lockmode))
{
Oid relid = locktag->locktag_field2;
...
for (uint32 i = 0; i < ProcGlobal->allProcCount; i++)
{
PGPROC *proc = GetPGProcByNumber(i);
...
for (j = 0; j < FP_LOCK_SLOTS_PER_GROUP; j++)
{
...
/* Look for an allocated slot matching the given relid. */
if (relid != proc->fpRelId[f])
continue;
lockmask = FAST_PATH_GET_BITS(proc, f);
if (!lockmask)
continue;
lockmask <<= FAST_PATH_LOCKNUMBER_OFFSET;
/*
* There can only be one entry per relation, so if we found it
* and it doesn't conflict, we can skip the rest of the slots.
*/
if ((lockmask & conflictMask) == 0)
break;
/* Conflict! */
GET_VXID_FROM_PGPROC(vxid, *proc);
if (VirtualTransactionIdIsValid(vxid))
vxids[count++] = vxid;
/* else, xact already committed or aborted */
break;
}
...
}
}Note that it does not hold the partition lock while scanning the fast-path
arrays — it takes each backend’s fpInfoLock in shared mode. The comment
acknowledges the resulting fuzziness:
This is all a bit fuzzy, because new locks could be taken after we’ve visited a particular partition, but the callers had better be prepared to deal with that anyway, since the locks could equally well be taken between the time we return the value and the time the caller does something with it.
The wait queue
When a conflict is real, the waiter joins the lock’s queue.
JoinWaitQueue() (in src/backend/storage/lmgr/proc.c) is where the
priority-queue logic lives:
/*
* JoinWaitQueue -- join the wait queue on the specified lock
*
* It's not actually guaranteed that we need to wait when this function is
* called, because it could be that when we try to find a position at which
* to insert ourself into the wait queue, we discover that we must be inserted
* ahead of everyone who wants a lock that conflict with ours. In that case,
* we get the lock immediately. Because of this, it's sensible for this function
* to have a dontWait argument, despite the name.
...
* NOTES: The process queue is now a priority queue for locking.
*/The queue is a doubly-linked list with a “conflict mask ahead of me” accumulator. The interesting part is the reordering rule:
/*
* Determine where to add myself in the wait queue.
*
* Normally I should go at the end of the queue. However, if I already
* hold locks that conflict with the request of any previous waiter, put
* myself in the queue just in front of the first such waiter. This is not
* a necessary step, since deadlock detection would move me to before that
* waiter anyway; but it's relatively cheap to detect such a conflict
* immediately, and avoid delaying till deadlock timeout.
*/So the queue is not FIFO; it’s ordered by conflict relationships. A backend that would block an earlier waiter jumps ahead of it. This avoids a whole class of deadlocks that would otherwise only be detected by the (slow, one-second-granularity) deadlock timeout.
The insertion itself:
/*
* Insert self into queue, at the position determined above.
*/
if (insert_before)
dclist_insert_before(waitQueue, &insert_before->waitLink, &MyProc->waitLink);
else
dclist_push_tail(waitQueue, &MyProc->waitLink);
lock->waitMask |= LOCKBIT_ON(lockmode);then the waiter sleeps in ProcSleep(), which is where the deadlock
detector’s timer lives:
/*
* ProcSleep -- put process to sleep waiting on lock
*
* This must be called when JoinWaitQueue() returns PROC_WAIT_STATUS_WAITING.
* Returns after the lock has been granted, or if a deadlock is detected. Can
* also bail out with ereport(ERROR), if some other error condition, or a
* timeout or cancellation is triggered.
*/When a lock is released, ProcLockWakeup() walks the queue and wakes
everyone who can now proceed:
/*
* ProcLockWakeup -- routine for waking up processes when a lock is
* released (or a prior waiter is aborted). Scan all waiters
* for lock, waken any that are no longer blocked.
*
* The appropriate lock partition lock must be held by caller.
*/
void
ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock)
{
dclist_head *waitQueue = &lock->waitProcs;
LOCKMASK aheadRequests = 0;
dlist_mutable_iter miter;
if (dclist_is_empty(waitQueue))
return;
dlist_foreach_modify(miter, waitQueue)
{
PGPROC *proc = dlist_container(PGPROC, waitLink, miter.cur);
LOCKMODE lockmode = proc->waitLockMode;
/*
* Waken if (a) doesn't conflict with requests of earlier waiters, and
* (b) doesn't conflict with already-held locks.
*/
if ((lockMethodTable->conflictTab[lockmode] & aheadRequests) == 0 &&
!LockCheckConflicts(lockMethodTable, lockmode, lock,
proc->waitProcLock))
{
/* OK to waken */
GrantLock(lock, proc->waitProcLock, lockmode);
/* removes proc from the lock's waiting process queue */
ProcWakeup(proc, PROC_WAIT_STATUS_OK);
}
else
{
/*
* Lock conflicts: Don't wake, but remember requested mode for
* later checks.
*/
aheadRequests |= LOCKBIT_ON(lockmode);
}
}
}The aheadRequests accumulator is the queue’s ordering invariant, and it’s
the same trick as in JoinWaitQueue(): a waiter is only woken if it
conflicts with nothing ahead of it in the queue.
Deadlock detection
The deadlock detector lives in src/backend/storage/lmgr/deadlock.c and
runs when deadlock_timeout (default 1 second) expires while waiting. It
takes all the partition locks:
/*
* Acquire exclusive lock on the entire shared lock data structures. Must
* grab LWLocks in partition-number order to avoid LWLock deadlock.
*
* Note that the deadlock check interrupt had better not be enabled
* anywhere that this process itself holds lock partition locks, else this
* will wait forever. Also note that LWLockAcquire creates a critical
* section, so that this routine cannot be interrupted by cancel/die
* interrupts.
*/
for (i = 0; i < NUM_LOCK_PARTITIONS; i++)
LWLockAcquire(LockHashPartitionLockByIndex(i), LW_EXCLUSIVE);which is why the deadlock check is a heavyweight, rare operation and not something you want on the hot path.
Because the deadlock detector knows nothing about the fast path (as the
comment in lock.c says explicitly: “The deadlock detector does not know
anything about the fast path mechanism, so any locks that might be involved
in a deadlock must be transferred from the fast-path queues to the main lock
table”), the fast path is safe: a lock that might be involved in a
deadlock is guaranteed to be in the heavyweight table by the time the
detector looks.
Sizing the lock table
The shared lock table is sized from max_locks_per_transaction:
#define NLOCKENTS() \
mul_size(max_locks_per_xact, add_size(MaxBackends, max_prepared_xacts))and the GUC:
{ name => 'max_locks_per_transaction', type => 'int', context => 'PGC_POSTMASTER', group => 'LOCK_MANAGEMENT',
short_desc => 'Sets the maximum number of locks per transaction.',
long_desc => 'The shared lock table is sized on the assumption that at most "max_locks_per_transaction" objects per server process or prepared transaction will need to be locked at any one time.',
variable => 'max_locks_per_xact',
boot_val => '128',
min => '10',
max => 'INT_MAX',
},So the table holds 128 × (MaxBackends + max_prepared_xacts) LOCK
entries, and twice that many PROCLOCK entries. Running out produces:
if (reportMemoryError)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of shared memory"),
errhint("You might need to increase \"%s\".", "max_locks_per_transaction")));The same GUC also sizes the fast path:
void
InitializeFastPathLocks(void)
{
/* Should be initialized only once. */
Assert(FastPathLockGroupsPerBackend == 0);
/*
* Based on the max_locks_per_transaction GUC, as that's a good indicator
* of the expected number of locks, figure out the value for
* FastPathLockGroupsPerBackend. This must be a power-of-two. We cap the
* value at FP_LOCK_GROUPS_PER_BACKEND_MAX and insist the value is at
* least 1.
*
* The default max_locks_per_transaction = 128 means 8 groups by default.
*/
FastPathLockGroupsPerBackend =
Max(Min(pg_nextpower2_32(max_locks_per_xact) / FP_LOCK_SLOTS_PER_GROUP,
FP_LOCK_GROUPS_PER_BACKEND_MAX), 1);
/* Validate we did get a power-of-two */
Assert(FastPathLockGroupsPerBackend ==
pg_nextpower2_32(FastPathLockGroupsPerBackend));
}128 / 16 = 8 groups, 8 × 16 = 128 slots per backend. Raising
max_locks_per_transaction gives both a bigger heavyweight table and
more fast-path slots — the two are sized from the same knob because they’re
sized from the same assumption.
Caveats
The fast path is only for relation locks in the current database. Shared
relations (locktag_field1 == InvalidOid), objects, advisory locks, tuple
locks, transaction locks — none of them are fast-path eligible.
The fast path is only for the three weak modes. AccessShareLock,
RowShareLock, RowExclusiveLock. ShareUpdateExclusiveLock is excluded
because it self-conflicts; everything stronger conflicts with the fast-path
modes and therefore disables the fast path.
The fast path has a per-backend slot limit. With the default configuration that’s 128 relation slots. A session touching more than that many relations in one transaction falls back to the heavyweight table, and the fallback is counted:
pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type);which is surfaced in the pg_stat_lock view, one row per lock tag type,
with a fastpath_exceeded column.
Taking a strong lock walks every backend. FastPathTransferRelationLocks()
iterates over ProcGlobal->allProcCount. On a cluster with hundreds of
backends, a stream of ALTER TABLEs is measurably more expensive than the
per-lock cost suggests.
Lock upgrades are heavyweight. Because the local lock table is keyed by
(tag, mode), asking for a different mode on the same object is a
different entry. If both modes are weak, the second one just sets another
bit in the same fast-path slot; if the second mode is strong, the whole
fast-path machinery for that relation is torn down and migrated into the
heavyweight table. SELECT then UPDATE on the same table therefore takes
two locks, not one.
Deadlock detection is a timeout, not a trap. deadlock_timeout
defaults to 1 second. A deadlock is detected when the waiter notices the
timeout and runs the detector, which then takes all sixteen partition
locks. Deadlock detection is therefore not instantaneous, and it is not free.
max_locks_per_transaction is a startup parameter. It’s PGC_POSTMASTER,
so changing it requires a restart, and it sizes shared memory, so raising it
costs shared memory proportional to the number of backends.
Locks are not released on error mid-acquisition. Well — they are, but
the locallock cleanup is subtle: the comment in lock.h warns that “a
locallock object can be left over from a failed lock acquisition attempt”,
in which case its lock/proclock pointers are garbage if nLocks == 0.
What it all means in practice
Most lock contention is imaginary. If you look at pg_locks and see
thousands of AccessShareLock rows, that is not thousands of contended
locks — those are just holders, and most of them are on the fast path.
pg_locks shows both worlds, and the fastpath column tells you which is
which:
TupleDescInitEntry(tupdesc, (AttrNumber) 15, "fastpath",
BOOLOID, -1, 0);A row with fastpath = true was never in the heavyweight table at all.
The heavyweight table’s partitioning is why PostgreSQL scales. Sixteen independently-locked hash partitions. Two sessions locking different tables rarely touch the same LWLock. This is invisible in the API and completely visible in the throughput numbers.
The local lock table is why repeated locking is free. A backend that already holds a lock on an object bumps a counter instead of touching shared memory. The cost of “the executor re-locks the relation for every tuple” is a local hash lookup.
The fast path is a bet that strong locks are rare. The bet is paid out
in full whenever a LOCK TABLE ... IN ACCESS EXCLUSIVE MODE or an ALTER TABLE shows up: the fast path is disabled for that hash partition and
every backend’s fast-path locks on that relation are migrated into the
heavyweight table. If your workload is dominated by DDL, the fast path buys
you nothing and costs you the transfer.
max_locks_per_transaction is a shared-memory sizing knob, not a
performance knob. Raising it gives you more heavyweight slots and more
fast-path slots. It doesn’t make locking faster; it makes “out of shared
memory” go away. If you’re seeing that error, the real question is why one
transaction needs more than 128 locks — usually a partition of a table being
attached, or a prepared transaction holding locks forever.
Deadlocks are a queue-ordering problem, not just a timeout problem. The
wait queue is a priority queue keyed on conflict relationships, and
JoinWaitQueue() deliberately jumps ahead of waiters you would block. This
is why PostgreSQL rarely reports a deadlock that “shouldn’t” happen.
The fast path is invisible in the API and visible in the source. The
whole mechanism is about not taking a lock. Every function that looks like
it should be simple — FastPathGrantRelationLock, FastPathUnGrantRelationLock
— is small, and the complexity is concentrated in the interaction between
the two worlds: the strong-lock counter, the transfer loop, and the
locallock->lock == NULL convention that means “this lock is on the fast
path”.
Further reading
src/include/storage/lock.h—LOCK,PROCLOCK,LOCALLOCK, the partition macros, and the local-lock-table rationale.src/include/storage/locktag.h— theLOCKTAGstruct, theLockTagTypeenum, and everySET_LOCKTAG_*macro.src/include/storage/lockdefs.h— the eight lock modes and their meanings.src/backend/storage/lmgr/lock.c— the heavyweight lock manager, the fast path,LockAcquireExtended(),LockCheckConflicts(),GetLockConflicts().src/backend/storage/lmgr/proc.c—JoinWaitQueue(),ProcSleep(),ProcLockWakeup(),BecomeLockGroupLeader().src/backend/storage/lmgr/deadlock.c— the deadlock detector.src/backend/utils/adt/lockfuncs.c—pg_lock_status()(thepg_locksview),pg_blocking_pids(), and the advisory-lock functions.src/backend/storage/lmgr/README— the authoritative prose description of all four lock mechanisms (spinlocks, LWLocks, heavyweight locks, predicate locks), including the “Fast Path Locking” section.src/include/storage/proc.h— thePGPROCfast-path fields and theFP_LOCK_*constants.src/backend/utils/init/postinit.c—InitializeFastPathLocks().src/include/storage/lwlock.h—NUM_LOCK_PARTITIONSand the LWLock array layout.- The
pg_locksview, insrc/backend/catalog/system_views.sqlandsrc/backend/utils/adt/lockfuncs.c.