首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> 關於C

Linux 多線程自旋鎖實現:原子操作、架構相關優化與死鎖風險分析

     編輯:關於C

自旋鎖是一種忙等待同步原語,適用於臨界區極短、競爭不激烈、不可睡眠(如中斷上下文)的場景。本文分析一個基於 GCC 内建函數與 x86 内聯彙編的自旋鎖實現,涵蓋可重入支持、CPU 讓出優化、信號處理死鎖風險。

核心數據結構


typedef struct {
    volatile unsigned long tid;   // 持有鎖的線程 ID
    volatile unsigned long lock;  // 鎖標誌 (0/1)
    volatile unsigned long recur; // 重入計數
} spin_lock_t;

原子操作抽象層

GCC ≥ 4.1 內建函數(推薦,跨架構)


#define atomic_test_and_set(a)  __sync_lock_test_and_set(a, 1)  // 原子交換並返回舊值
#define atomic_clear(a)         __sync_lock_release(a)          // 釋放語義存儲 0
  • __sync_lock_test_and_set:完整內存屏障 + 原子交換,獲取鎖語義。
  • __sync_lock_release:釋放語義屏障 + 存儲 0,釋放鎖語義。

x86/x86_64 內聯彙編兜底


// 原子比較交換獲取鎖
inline static unsigned long atomic_test_and_set(volatile unsigned long* a) {
    const unsigned long n = 1, old = 0;
    unsigned long ret;
    __asm__ __volatile__(
        "lock; cmpxchgl %3, %1;"
        : "=a"(ret)
        : "m"(*a), "a"(old), "r"(n)
        : "memory", "cc"
    );
    return ret; // 返回舊值,0=成功獲取,1=已被持有
}

// 原子交換釋放鎖
inline static void atomic_clear(volatile unsigned long* a) {
    unsigned long ret, n = 0;
    __asm__ __volatile__(
        "lock; xchgl %0, %1"
        : "=r"(ret)
        : "m"(*a), "0"(n)
        : "memory"
    );
}
  • lock 前綴:總線鎖定/緩存鎖定,保證多核原子性。
  • cmpxchgl:比較 EAX 與內存,相等則存入新值並置 ZF=1,否則讀入內存值到 EAX。
  • xchgl:原子交換寄存器與內存。

鎖操作實現

初始化


inline static void spin_lock_init(spin_lock_t* lk) {
    lk->tid = 0; lk->lock = 0; lk->recur = 0;
}

加鎖(支持可重入 + 指數退讓)


#define SPIN_YIELD_INTERVAL 63U  // 每 64 次自旋調用一次 sched_yield()

inline static int spin_lock_lock(spin_lock_t* lk) {
    pid_t now = syscall(SYS_gettid); // 獲取內核線程 ID (TID)
    int c = 0;
    for (;;) {
        if (!atomic_test_and_set(&lk->lock)) { // 成功獲取
            lk->tid = now;
            lk->recur = 1;
            return 0;
        }
        if (lk->tid == now) // 可重入:同一線程再次加鎖
            return ++lk->recur;

        if (!(++c & SPIN_YIELD_INTERVAL))
            sched_yield(); // 主動讓出 CPU,減少總線爭用
    }
}

解鎖


inline static int spin_lock_unlock(spin_lock_t* lk) {
    unsigned long now = (unsigned long)syscall(SYS_gettid);
    if (lk->tid != now) return -1; // 非持有者嘗試解鎖
    if (--lk->recur == 0) {        // 最後一層重入釋放
        lk->tid = 0;
        atomic_clear(&lk->lock);
    }
    return 0;
}

關鍵設計點解析

  1. 可重入性:記錄持有者 tid 與 recur 計數,同一線程多次加鎖只需原子操作一次。
  2. 指數退讓:SPIN_YIELD_INTERVAL=63 使 sched_yield() 每 64 次循環調用一次,平衡自旋開銷與調度延遲。
  3. 內存序:GCC 内建函數/ lock 指令提供全屏障,確保臨界區內存操作不被重排到鎖外。
  4. TID 獲取:用 syscall(SYS_gettid) 而非 getpid() 或 pthread_self(),因後兩者在線程組/用戶態庫中可能不唯一或開銷大。

重大風險:信號處理函數中的死鎖


// 註釋中的警告:
// 如果在信號處理函數中使用該鎖,可能會死鎖。
// 場景:
// 1. 主線程持有鎖,進入臨界區
// 2. 信號到達,中斷主線程執行信號處理函數
// 3. 信號處理函數嘗試獲取同一把鎖 → 自旋等待
// 4. 主線程無法繼續釋放鎖 → 永久死鎖

緩解方案:

  • 信號處理函數中絕不使用自旋鎖(或任何可能阻塞的鎖)。
  • 若必須共享數據,用 sig_atomic_t、無鎖環形緩衝區、或將信號轉為管道通知專用線程處理。
  • 使用 pthread_sigmask 在臨界區屏蔽信號(但會延遲信號遞送)。

與 pthread_mutex 對比

特性自旋鎖pthread_mutex
等待方式忙等待 (CPU 佔用)阻塞睡眠 (上下文切換)
適用臨界區極短 (< 1µs)任意長度
中斷上下文可用 (無睡眠)不可用
可重入需手動實現PTHREAD_MUTEX_RECURSIVE
優先級反轉無保護支持優先級繼承/天花板

總結

這個自旋鎖實現展示了原子操作抽象、架構相關優化、可重入邏輯、退讓策略等核心要素。但信號上下文死鎖風險是其致命短板,生產環境優先考慮 pthread_mutex、pthread_rwlock、無鎖數據結構或更高層並發模式(Actor、CSP)。僅在內核模塊、實時系統、極低延遲庫等專用場景謹慎使用,並嚴格審計信號處理路徑。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved