PostgreSQL中的SpinLock

PostgreSQL中的SpinLock

1. What is SpinLock?

SpinLock也就是我們常說的自旋鎖,其顯著的特點就是“死等”,需要獲取SpinLock的線程會一直主動地check能否獲取得到鎖,直到獲取到鎖後線程纔會繼續執行下面的邏輯,這把鎖會一直被這個線程持有,直到線程自己主動釋放。因此,如果我們的應用場景,線程在鎖的獲取上只會被阻塞非常短的一段時間(或者鎖在獲取後馬上會被釋放),那麼SpinLock的使用可以減少CPU對於進程重新調度(rescheduling)和上下文切換的開銷(context swithing),例如:OS kernels大量使用了SpinLock。反之,如果線程長久地持有SpinLock,那麼SpinLock就會浪費大量的系統資源。因爲,其它在在等待鎖的線程,就會一直處於“spinnig”狀態,相當於空跑CPU,白白地浪費CPU資源。

在實現SpinLock時,需要考慮到多線程對於lock本身併發訪問的問題,因爲這也會產生所謂的race condition。當前,可能是唯一比較好的實現方式是使用具有原子性的彙編語言指令集test-and-set。

test-and-set指令會向一塊給定的內存寫1,完成後返回這塊內存寫之前的值。這條CPU指令是具有原子性的,如果有多進程同時更新一塊內存時,某一時刻,有且僅會有1條指令被允許在這塊內存地址上進行操作。
因此,使用test-and-set指令實現SpinLock只需要下面一段簡單的代碼即可。當old value爲0的時候獲取到鎖,否則會一直spin去試圖加鎖。

void Lock(boolean *lock) { 
    while (test_and_set(lock) == 1); 
}

然而,如果程序所運行的CPU架構不支持test-and-set指令集,那麼就需要使用high-level的編程語言來實現SpinLock。比如:併發控制算法(Peterson’s algorithm),信號量等。在這兩種實現方式中,基於CPU指令test-and-set實現的SpinLock,效率更高。

2. PostgreSQL中SpinLock實現

在PostgreSQL中,有兩種實現SpinLock的方式:1)CPU指令集test-and-set;2)使用PG信號量。使用test-and-set指令集實現的SpinLock在文件s_lock.h和s_lock.c中。而使用PG信號量實現的SpinLock在文件spin.c中。

test-and-set實現的加鎖邏輯。代碼如下:

int
s_lock(volatile slock_t *lock, const char *file, int line, const char *func)
{
	SpinDelayStatus delayStatus;

        // 初始化SpinLock的狀態信息,如spin的次數等
	init_spin_delay(&delayStatus, file, line, func);

	while (TAS_SPIN(lock))
	{
                // spins,在cpu級別有一個delay時間,另外當spin次數大於100,
                // 在此函數中會隨機休眠1ms到1s
		perform_spin_delay(&delayStatus);
	}

        // 獲取鎖後,根據delay的結果調整進入休眠的spin次數,如果,在獲取鎖的時候
        // 沒有休眠過,那麼可以把進入休眠spin的次數調大。如果休眠過,表示鎖競爭大,
        // 就把進入休眠spin的次數降低,減少CPU消耗。
	finish_spin_delay(&delayStatus);

	return delayStatus.delays;
}

放鎖邏輯,跟CPU硬件相關,包括編譯器,因爲,CPU和編譯器可能會重新編排指令,產生亂序訪問內存,因此在執行S_UNLOCK這個宏之前,必須保證在這個宏命令issue前load/store命令被先執行完了(當然TAS也是,只是對於上面的加鎖函數s_lock來說是平臺無關的,而裏面的宏TAS_SPIN也需要保證issue在這個宏之後的load/store命令必須在這個宏之後執行,這樣就能保證,對於臨界區的訪問被鎖包住):

#define S_UNLOCK(lock)	\
	do { __memory_barrier(); *(lock) = 0; } while (0)

如果所運行的平臺沒有test-and-set指令,則使用PG信號量實現的SpinLock。PG中默認有128個信號量用於SpinLock(所以系統最大同時可用的SpinLock的數量爲128),另外64個信號量用於atomic operation,因爲原子操作的實現依賴於SpinLock,PG信號量實現的加鎖邏輯如下:

int
tas_sema(volatile slock_t *lock)
{
	int			lockndx = *lock;

	if (lockndx <= 0 || lockndx > NUM_SPINLOCK_SEMAPHORES)
		elog(ERROR, "invalid spinlock number: %d", lockndx);
	/* Note that TAS macros return 0 if *success* */
	return !PGSemaphoreTryLock(SpinlockSemaArray[lockndx - 1]);
}

bool
PGSemaphoreTryLock(PGSemaphore sema)
{
	DWORD		ret;

	ret = WaitForSingleObject(sema, 0);

	if (ret == WAIT_OBJECT_0)
	{
		/* We got it! */
		return true;
	}
	else if (ret == WAIT_TIMEOUT)
	{
		/* Can't get it */
		errno = EAGAIN;
		return false;
	}

	/* Otherwise we are in trouble */
	ereport(FATAL,
			(errmsg("could not try-lock semaphore: error code %lu",
					GetLastError())));

	/* keep compiler quiet */
	return false;
}

其中WaitForSingleObject是win32系統函數,其作用相當於我們在操作系統課程上學過的信號量的知識中獲得信號量操作,會將某個信號量的值減1,在SplinLock中,信號量的值初始化爲1。所以,之後再有線程需要獲得鎖的時候,就會在這個函數中等待。而放鎖的邏輯如下,也就是相當於會釋放信號,將信號量+1,允許其它線程獲得該信號量:

void
s_unlock_sema(volatile slock_t *lock)
{
	int			lockndx = *lock;

	if (lockndx <= 0 || lockndx > NUM_SPINLOCK_SEMAPHORES)
		elog(ERROR, "invalid spinlock number: %d", lockndx);
	PGSemaphoreUnlock(SpinlockSemaArray[lockndx - 1]);
}

/*
 * PGSemaphoreUnlock
 *
 * Unlock a semaphore (increment count)
 */
void
PGSemaphoreUnlock(PGSemaphore sema)
{
	if (!ReleaseSemaphore(sema, 1, NULL))
		ereport(FATAL,
				(errmsg("could not unlock semaphore: error code %lu",
						GetLastError())));
}

3. When SpinLock is used in PostgreSQL?

從上文的介紹上看,SpinLock不能用於需要長久持有鎖的邏輯。因此,在PostgreSQL中,SpinLock主要用於對於臨界變量的併發訪問控制,所保護的臨界區通常是簡單的賦值語句,讀取語句等。另外,在PG中,SpinLock沒有等待隊列、死鎖檢測機制,在事務結束之後不會自動釋放,需要每次顯式釋放。

轉自

PostgreSQL中的SpinLock

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章