文章目錄
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_count
及printf
輸出,實際結果沒有符合預期。
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節的“自旋鎖使用原則”。至此,互斥鎖、讀寫鎖、自旋鎖描述完成,三者的特點差異,羅列出下表比較。
互斥鎖、讀寫鎖、自旋鎖對比
主要特點 | 引起線程睡眠 | 適用範圍 | 資源開銷 | |
---|---|---|---|---|
互斥鎖 | 互斥 | 是 | 一般互斥訪問 | 普通 |
讀寫鎖 | 讀讀共享 | 是 | 多讀少寫 | 普通 |
自旋鎖 | 自旋等待 | 否 | 加鎖時間極短 | 低開銷 |