【Linux應用編程】POSIX線程互斥與同步機制—自旋鎖



1 前言

  前面文章分別描述了互斥鎖和讀寫鎖的含義、屬性、使用原則、使用場景以及使用方法。本文描述除了互斥鎖、讀寫鎖外的第三種鎖——自旋鎖


2 自旋鎖

  自旋鎖( Spin lock )是線程間互斥的一種機制。自旋鎖本質是一把鎖,實現的功能與互斥鎖完全一樣,都是任一時刻只允許一個線程持有鎖,達到互斥訪問共享資源的目的。唯一的不同之處在於兩者的調度策略不一樣,線程申請不到互斥鎖時,會使線程睡眠讓出cpu資源,獲得互斥鎖後線程喚醒繼續執行;而自旋鎖阻塞後不會引起線程睡眠,一直佔用cpu資源直至獲得自旋鎖。自旋鎖是一種輕量級的鎖,相比互斥鎖,資源開銷更小,在極短時間的加鎖,自旋鎖是最理想的選擇,可以提高效率。


2.1 自旋鎖特點

  自旋鎖的特點與其命名匹配,線程獲取不到鎖時就是一直處於忙等待(原地打轉?)狀態,佔用cpu的同時又不能處理任何任務。根據自旋鎖的特點,自旋鎖適用於佔用鎖時間極短的場景,長時間佔用自旋鎖會降低系統性能。如果訪問資源比較耗時,需長時間持有鎖的場景,則需考慮其他互斥機制。

  • 用於線程互斥
  • 阻塞一直佔用cpu資源
  • 不可引起線程睡眠
  • 輕量級的鎖
  • 資源開銷小,包括創建、持有、釋放過程

2.2 自旋鎖適用場景

   自旋鎖一開始是爲防止多核處理器(SMP)併發帶來競態而引入的一種互斥機制。 自旋鎖在用戶態使用得比較少,在內核態下,常見的驅動開發會經常用到自旋鎖。內核態下的自旋鎖使用可以參考文章併發與競態(如何選擇合適的保護機制)。自旋鎖適用於短期內進行輕量級的鎖定。

  • 互斥資源訪問時間極短(加鎖時間短),小於2次上下文切換的時間
  • 特殊場景,不希望掛起線程

2.3 自旋鎖使用原則

  自旋鎖與互斥鎖一樣,自旋鎖使用原則可以參考互斥鎖的使用原則,互斥鎖的使用原則也是自旋鎖的基本使用原則。

  • 加鎖時間極短,並及時釋放鎖
  • 禁止嵌套(遞歸)申請持有自旋鎖,否則導致死鎖
  • 避免過多的自旋鎖申請,防止cpu資源浪費

注:

申請持有自旋鎖時會一直佔用cpu,如果嵌套或者遞歸申請自旋鎖,在第二層申請鎖時,由於鎖被第一層持有,第二層獲取不到鎖一直處於等待狀態並佔用cpu,程序也無法跳出到最外層釋放鎖,導致死鎖發生。因此,遞歸程序中使用自旋鎖需謹慎


3 自旋鎖使用

  自旋鎖使用的基本步驟爲:

【1】創建自旋鎖實例

【2】初始化自旋鎖

【3】持有自旋鎖

【4】釋放自旋鎖

【5】銷燬自旋鎖實例


3.1 創建自旋鎖

  posix線程自旋鎖以pthread_spinlock_t數據結構表示。自旋鎖實例可以用靜態和動態創建。

pthread_spinlock_t spinlock;

3.2 初始化自旋鎖

  自旋鎖初始化只支持使用pthread_rwlock_init函數進行動態初始化 。

int pthread_spin_init(pthread_spinlock_t *spinlock, int pshared);
  • spinlock,自旋鎖實例地址,不能爲NULL

  • pshared,自旋鎖作用域

    PTHREAD_PROCESS_PRIVATE,進程內(創建者)作用域,只能用於進程內線程互斥

    PTHREAD_PROCESS_SHARED,跨進程作用域,用於系統所有線程間互斥

  • 返回,成功返回0,參數無效返回 EINVAL


3.3 自旋鎖上鎖(申請鎖)

  自旋鎖申請持有分爲阻塞方式和非阻塞方式,常用的一般是阻塞方式。


3.3.1 阻塞方式

int pthread_spin_lock(pthread_spinlock_t *spinlock);
  • spinlock,自旋鎖實例地址,不能爲NULL

  • 返回,成功返回0,參數無效返回 EINVAL

  如果自旋鎖還沒有被其他線程持有(上鎖),則申請持有自旋鎖的線程獲得鎖。如果自旋鎖被其他線程持有,則線程一直處於等待狀態(佔用cpu),直到持自旋鎖的線程解鎖後,線程獲得鎖繼續執行。不允許遞歸嵌套申請自旋鎖,否則導致死鎖。


3.3.2 非阻塞方式

int pthread_spin_trylock(pthread_spinlock_t spinlock*);
  • spinlock,自旋鎖實例地址,不能爲NULL
  • 返回
返回值 描述
0 成功
EINVAL 參數無效
EDEADLK 死鎖
EBUSY 鎖被其他線程持有

  調用該函數會立即返回,不會阻塞等待。實際應用可以根據返回狀態執行不同的任務操作。


3.4 自旋鎖釋放

int pthread_spin_unlock(pthread_spinlock_t *spinlock);
  • spinlock,自旋鎖實例地址,不能爲NULL
  • 返回
返回值 描述
0 成功
EINVAL 參數無效
EDEADLK 死鎖
EBUSY 鎖被其他線程持有

  自旋鎖持有後必須及時釋放,不允許多次釋放鎖。


3.5 自旋鎖銷燬

int pthread_spinlock_destroy(pthread_spinlock_t *spinlock);
  • spinlock,自旋鎖實例地址,不能爲NULL
  • 返回
返回值 描述
0 成功
EINVAL spinlock已被銷燬過,或者spinlock爲空
EBUSY 自旋鎖被其他線程使用

  pthread_spinlock_destroy用於銷燬一個已經使用動態初始化的自旋鎖。銷燬後的自旋鎖處於未初始化狀態,自旋鎖的屬性和控制塊參數處於不可用狀態。使用銷燬函數需要注意幾點:

  • 已銷燬的自旋鎖,可以使用pthread_spinlock_init重新初始化使用
  • 不能重複銷燬已銷燬的自旋鎖
  • 沒有線程持有自旋鎖時,才能銷燬

3.6 寫個例子

  代碼實現功能:

  • 創建兩個線程
  • 兩個線程分別對全局變量訪問,並輸出到終端
  • 期望結果,線程1輸出結果“ 1 2 3 4 5”,線程2輸出結果“5 4 3 2 1”
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include "pthread.h" 

#define	USE_SPINLOCK 1	/* 是否使用自旋鎖,使用,0不使用 */

#if USE_SPINLOCK
pthread_spinlock_t spinlock;
#endif

static int8_t g_count = 0;

void *thread0_entry(void *data)  
{
	uint8_t  i =0;

#if USE_SPINLOCK
	pthread_spin_lock(&spinlock);
#endif
	for (i = 0;i < 5;i++)
	{
		g_count ++;
		printf("%d ", g_count);
		usleep(100);
	}
	printf("\r\n");
#if USE_SPINLOCK
	pthread_spin_unlock(&spinlock);
#endif
}

void *thread1_entry(void *data)  
{
	uint8_t  i =0;

	usleep(10);	/* 讓線程0先執行 */
#if USE_SPINLOCK
	pthread_spin_lock(&spinlock);
#endif
	for (i = 0;i < 5;i++)
	{
		printf("%d ", g_count);
		g_count--;
		usleep(100);
	}
	printf("\r\n");
#if USE_SPINLOCK
	pthread_spin_unlock(&spinlock);
#endif
}

int main(int argc, char **argv)  
{
	pthread_t thread0,thread1; 
    void *retval; 
    
#if USE_SPINLOCK
	pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);/* 進程內作用域 */
#endif
    pthread_create(&thread0, NULL, thread0_entry, NULL);
	pthread_create(&thread1, NULL, thread1_entry, NULL);
    pthread_join(thread0, &retval);
    pthread_join(thread1, &retval);
	
	return 0;
 }

不加自旋鎖的結果

  由於不使用鎖,線程間併發執行,"同時"訪問全局變量g_countprintf輸出,實際結果沒有符合預期。

acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/spinlock$ gcc spinlock.c -o spinlock -lpthread
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/spinlock$ ./spinlock
1 2 2 2 2 2 2 1 1 1

使用自旋鎖的結果

  線程0持有鎖之後,訪問執行完後才釋放鎖,線程2申請到鎖,輸出結果正確。

acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/spinlock$ gcc spinlock.c -o spinlock -lpthread
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/spinlock$ ./spinlock
1 2 3 4 5 
5 4 3 2 1 

  代碼中,對printf函數加鎖,實際使用是不允許的,違反了加鎖的原則,這裏只是模擬場景測試。


4 自旋鎖屬性

  自旋鎖是一種輕量級的鎖,屬性只有一個“作用域”,在調用pthread_spin_init函數初始化自旋鎖時指定作用域範圍。自旋鎖作用域表示自旋鎖的互斥作用範圍,分爲進程內(創建者)作用域PTHREAD_PROCESS_PRIVATE和跨進程作用域PTHREAD_PROCESS_SHARED。進程內作用域只能用於進程內線程互斥,跨進程可以用於系統所有線程間互斥。


5 總結

  自旋鎖實現的功能與互斥鎖一樣,都是用於線程間互斥訪問。自旋鎖是一種不會引起線程睡眠的輕量級鎖,適用於加鎖時間極短的場景,由於其資源開銷比互斥鎖低,在極短的加鎖場景使用自旋鎖效率會更高。自旋鎖的使用注意事項,結合互斥鎖文章2.3節的"互斥鎖使用原則",參考2.3節的“自旋鎖使用原則”。至此,互斥鎖、讀寫鎖、自旋鎖描述完成,三者的特點差異,羅列出下表比較。

互斥鎖、讀寫鎖、自旋鎖對比

主要特點 引起線程睡眠 適用範圍 資源開銷
互斥鎖 互斥 一般互斥訪問 普通
讀寫鎖 讀讀共享 多讀少寫 普通
自旋鎖 自旋等待 加鎖時間極短 低開銷
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章