一、 等待机制
如果通过冲突检测发现可以获得锁,则通过GrantLock和GrantLocalLock函数增加锁的引用计数。如果发现不能获得锁,则进入等待状态。
等待状态的判断通过WaitOnLock函数实现,调用关系是WaitOnLock函数 -> ProcSleep函数。ProcSleep函数一方面将当前事务加入等待队列,另一方面还要做死锁检测。
二、 ProcSleep函数
1. 函数主要流程图
补画了一个流程图,可以对照着看,之前看着看着下面代码的判断和循环就晕菜了。
2. 具体代码
函数调用栈如下:
/*
* ProcSleep -- put a process to sleep on the specified lock
* ProcSleep -- 让进程等待特定的锁
* Caller must have set MyProc->heldLocks to reflect locks already held
* on the lockable object by this process (under all XIDs).
* 调用者必须设置MyProc->heldLocks 表示本进程已获得的锁
* The lock table's partition lock must be held at entry, and will be held
* at exit.锁表的分区锁必须在进入函数时必须持有,在退出函数时也会持有
* Result: PROC_WAIT_STATUS_OK if we acquired the lock, PROC_WAIT_STATUS_ERROR if not (deadlock).成功获得锁则返回PROC_WAIT_STATUS_OK,失败则返回PROC_WAIT_STATUS_ERROR(出现死锁)
*
* ASSUME: that no one will fiddle with the queue until after we release the partition lock. 假设没有人会篡改该等待队列,直到我们释放分区锁
*
* NOTES: The process queue is now a priority queue for locking.该进程队列目前是对锁优先队列
*/
ProcWaitStatus
ProcSleep(LOCALLOCK *locallock, LockMethod lockMethodTable)
{
LOCKMODE lockmode = locallock->tag.mode;
LOCK *lock = locallock->lock;
PROCLOCK *proclock = locallock->proclock;
uint32 hashcode = locallock->hashcode;
LWLock *partitionLock = LockHashPartitionLock(hashcode);
PROC_QUEUE *waitQueue = &(lock->waitProcs);
LOCKMASK myHeldLocks = MyProc->heldLocks;
TimestampTz standbyWaitStart = 0;
bool early_deadlock = false;
bool allow_autovacuum_cancel = true;
bool logged_recovery_conflict = false;
ProcWaitStatus myWaitStatus;
PGPROC *proc;
PGPROC *leader = MyProc->lockGroupLeader;
int i;
/*
* 收集当前事务在locallock这个锁对象上持有的其他锁模式至 myHeldLocks。如果不是单进程事务(有并行),则需要将相同group中持有的该锁对象的锁模式一并收集。
*/
if (leader != NULL) //不是单进程事务
{
SHM_QUEUE *procLocks = &(lock->procLocks);
PROCLOCK *otherproclock;
otherproclock = (PROCLOCK *)
SHMQueueNext(procLocks, procLocks, offsetof(PROCLOCK, lockLink));
while (otherproclock != NULL)
{
if (otherproclock->groupLeader == leader)
myHeldLocks |= otherproclock->holdMask;
otherproclock = (PROCLOCK *)
SHMQueueNext(procLocks, &otherproclock->lockLink,
offsetof(PROCLOCK, lockLink));
}
}
/*
* 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.
* 将当前事务(或进程)加入等待队列也是有技巧的。通常,等待队列应该按照锁申请的顺序排列,将当前事务加入等待队列的队尾。但如果本事务A除了当前申请的锁模式,已经持有了该对象的其他锁模式,而等待队列中的事务B等待的锁模式与事务A持有的锁模式冲突,此时再将事务A插入等待者B的后面,就隐含死锁的风险,可以考虑将事务A插队到事务B的前面。
*/
if (myHeldLocks != 0) //当前事务在这个锁对象上还持有其他锁模式
{
LOCKMASK aheadRequests = 0;
// 从等待队列中获取一个事务,它是一个等待者
proc = (PGPROC *) waitQueue->links.next;
for (i = 0; i < waitQueue->size; i++) //循环检查等待队列
{
/*
* If we're part of the same locking group as this waiter, its
* locks neither conflict with ours nor contribute to
* aheadRequests. 如果当前事务(进程)跟等待队列的事务是同一个group的并行进程,那么它们之间不会有锁冲突,也不会对aheadRequests有什么用。取等待队列中的下一个事务,进入下一次循环即可。
*/
if (leader != NULL && leader == proc->lockGroupLeader)
{
proc = (PGPROC *) proc->links.next;
continue;
}
/* Must he wait for me? 如果非并行进程,判断等待者想要的锁模式与当前事务持有的是否冲突。即,它一定要等我吗*/
if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks)
{
/* Must I wait for him ? 反过来,我等的是不是这个等待队列的事务持有的锁模式,我一定要等它吗*/
if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks)
{
/*
* Yes, so we have a deadlock. Easiest way to clean up
* correctly is to call RemoveFromWaitQueue(), but we
* can't do that until we are *on* the wait queue. So, set
* a flag to check below, and break out of loop. Also,
* record deadlock info for later message.
* 如果两者都是,那就死锁了。最简单的方式是调用RemoveFromWaitQueue清理,但必须等到我们*在*等待队列时才能执行,因此,我们只是打一个死锁的标记,然后退出循环。另外,还要记录死锁信息。
*/
RememberSimpleDeadLock(MyProc, lockmode, lock, proc);
early_deadlock = true;
break;
}
/* I must go before this waiter. 我必须在它之前。
* aheadRequests 记录的是在等待队列中,处于proc这个等待者前面,所有等待者等待的模式的并集
* 如果当前锁模式和proc前面所有的锁模式都不冲突,且在主锁表也未检查到冲突,则获取锁成功
*/
if ((lockMethodTable->conflictTab[lockmode] & aheadRequests) == 0 &&
!LockCheckConflicts(lockMethodTable, lockmode, lock,
proclock))
{
/* Skip the wait and just grant myself the lock. 跳过等待直接获取锁 */
GrantLock(lock, proclock, lockmode);
GrantAwaitedLock();
return PROC_WAIT_STATUS_OK;
}
/* Break out of loop to put myself before him. 否则,要找到等待队列中第一个与我冲突的事务,记住这个proc,跳出循环,把当前事务(我)插到它前面 */
break;
}
/* Nope, so advance to next waiter,如果它不需要等待我,那么再换下一个等待队列的事务 */
aheadRequests |= LOCKBIT_ON(proc->waitLockMode); // 收集等待队列中等待的锁模式的并集
proc = (PGPROC *) proc->links.next; //再换下一个等待队列的事务
}
/*
* If we fall out of loop normally, proc points to waitQueue head, so
* we will insert at tail of queue as desired.
* 如果这么多个if都没匹配到,循环顺利完成了,此时proc会指向等待队列waitQueue的尽头,因此我们可以按照之前预期的,将当前事务插到等待队列的最末尾。
*/
}
else //如果当前事务没持有过锁对象的锁模式,获取等待队列的队首元素。也就是说,要把当前事务插到等待队列的头部。
{
/* I hold no locks, so I can't push in front of anyone. */
proc = (PGPROC *) &(waitQueue->links);
}
/*
* Insert self into queue, ahead of the given proc (or at tail of queue). 将其插入到proc前面,或者队列最末端
*/
SHMQueueInsertBefore(&(proc->links), &(MyProc->links));
waitQueue->size++; //等待队列长度加1
// 插入等待队列后,事务就开始进入等待状态
lock->waitMask |= LOCKBIT_ON(lockmode);
/* Set up wait information in PGPROC object, too */
MyProc->waitLock = lock;
MyProc->waitProcLock = proclock;
MyProc->waitLockMode = lockmode;
MyProc->waitStatus = PROC_WAIT_STATUS_WAITING;
/*
* If we detected deadlock, give up without waiting. This must agree with
* CheckDeadLock's recovery code.如果前面设置了early_deadlock标志,不需要再等待,直接返回报错
*/
if (early_deadlock)
{
RemoveFromWaitQueue(MyProc, hashcode);
return PROC_WAIT_STATUS_ERROR;
}
/* mark that we are waiting for a lock */
lockAwaited = locallock;
/*
* Release the lock table's partition lock.
*
* NOTE: this may also cause us to exit critical-section state, possibly
* allowing a cancel/die interrupt to be accepted. This is OK because we
* have recorded the fact that we are waiting for a lock, and so
* LockErrorCleanup will clean up if cancel/die happens.
*/
LWLockRelease(partitionLock);
这个函数下面还有很长一截关于InHotStandby状态下的处理,由于过于复杂,这里先跳过了。
等待状态的事务会在两种状态下被唤醒:
- 死锁检测触发超时机制,要进行下一轮死锁检测
- PGPROC->waitStatus不再是STATUS_WAITING状态,已经有其他事务释放了锁,当前事务被唤醒
参考
《PostgreSQL技术内幕:事务处理深度探索》第2章