自旋锁(spinlock),是操作系统和数据库的重要基石。可以说:没有自旋锁,就没有操作系统和数据库软件。自旋锁是PG其它类型的锁的基础。其它类型的锁都是用共享内存加自旋锁组成的。在本文中,我们对PG中的自旋锁机制做一个入门介绍。
英文单词spin的含义是turn or cause to turn or whirl around quickly,中文的意思就是“快速旋转”的意思。这个单词很容易让我们想到小时候打的陀螺。你用一个鞭子不停地抽打陀螺,它就会在地面或者冰面上快速旋转。反复地做一件事情,就是自旋锁的基本特征。
我们想象一下自己坐飞机的经历。当你内急,就会去上卫生间。但是飞机上的卫生间只有一个,所以就存在一个协调如何使用的问题。当你看到卫生间门上有一个标识牌,其上写着"Occupied",你就知道有人在使用卫生间,你能做的事情就是在门口等待,或者离开一会,再回来检查那个标识牌的文字是否变成了“vacancy",即”空闲“的意思。如果没有,你就会重复上面的动作,离开一会,再次检查标识牌。你的行为就是一个自旋锁的行为。
所谓自旋锁,就是一个共享内存单元,一个字节就足够了,它的值为0,表示某个共享资源R没有别的进程在使用,如果为1,则表示R正在被别的进程使用中。所以自旋锁L和共享资源R都是共享内存中的某些字节,L用来保护R,使得对R的访问是串行执行的,类似我们使用飞机上的卫生间也是先来后到,一次只能一个人使用卫生间。由于L的值只需要0和1,分别表示”空闲“和”占用“,所以L一般只要一个字节即可,考虑到对齐的原因,可以扩展到4字节或者8字节。R的大小就根据需要来定,没有什么限定。
当一个进程A想访问R,主要是往R中写入一些信息,再次提醒,R是一段共享内存,A会首先检查L中的值是多少。如果L=0,表示没有别的进程访问R,则A把L设置为1,表示我占用了,俗称“加锁”,然后A就可以放心大胆地访问R了。访问结束后,A再把L的值设为0,俗称“释放锁”。如果A发现L的值为1,表明另外一个进程B正在往R中写数据,A就就陷入一个无限循环,反复检查L的值什么时候变成0。B在访问R结束后,也会让L=0,表示释放自旋锁L。A的这种无限循环,反复检查L的值啥时候从1变成0的行为,就类似一个陀螺在快速旋转,这就是“自旋锁”名称的来历。
从上面的描述可以看出,如果B使用R很长时间,A就会陷入疯狂的循环中,白白浪费大量CPU资源,所以自旋锁一般适合对共享资源R进行快速访问的场景。譬如B仅仅是修改R中的少量字节。我们会在PG的源代码中看到类似如下的代码:
/*
* Update the WalWriterSleeping flag.
*/
void
SetWalWriterSleeping(bool sleeping)
{
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->WalWriterSleeping = sleeping;
SpinLockRelease(&XLogCtl->info_lck);
}
函数SetWalWriterSleeping()的逻辑就是设置共享内存XLogCtl->WalWriterSleeping的值,这个操作非常快。这种场景就非常适合自旋锁。所以任何进程想修改这个值时,先调用SpinLockAcquire()获得自旋锁XLogCtl->info_lck,操作完毕后,再调用SpinLockRelease()释放该自旋锁。
在上述代码中,自旋锁L就是XLogCtl->info_lck,而被保护的共享资源R就是XLogCtl->WalWriterSleeping。访问共享资源的模式就是三部曲:加锁,修改,释放锁。上述代码就是这种模式的很好的范例。
很自然,当一个进程执行SpinLockAcquire()时,主要的工作就是检查info_lck的值是否为0,如果是,则就执行下面的操作。如果不是,该进程就会陷入疯狂的循环中,即“自旋”状态。当然,因为对共享资源的访问操作速度极快,所以一个进程陷入疯狂自旋的几率是极小的。SpinLockRelease()函数的逻辑自然是让info_lck的值变成0。
关于SpinLockAcquire()和SpinLockRelease()的细节,我们在后面的文章中进行剖析。