1. 程式人生 > >自旋鎖,讀寫鎖和順序鎖的實現原理

自旋鎖,讀寫鎖和順序鎖的實現原理

並且 保護 表達 min 返回 create creat rwlock ini

常用的同步原語,到多核處理器時代鎖已經是必不可少的同步方式之一了。無論設計多優秀的多線程數據結構,都避不開有競爭的臨界區,此時高效的鎖顯得至關重要。鎖的顆粒度是框架/程序設計者所關註的,當然越細越好(也不盡然),同時不同的鎖往往也會體現出完全不同的效率,Linux有posix的pthread_mutex_t,Windows有CreateMutex創造的HANDLE,boost有mutex而且C++11也定義了std::mutex,這些鎖在獲取不到鎖時都會進入睡眠狀態(try_lock方法例外)並將CPU讓出來給其他的進程使用。但是在很多情況下,多個進程競爭的臨界區的代碼可能非常的短,例如:鎖住數據數據鎖,讀取數據,修改計數器,釋放臨界區,再用數據計算…在這種(臨界區很短的)情況下,用上面提到的那些鎖會導致繁瑣的用戶態和內核態的切換和繁重的進程切換,這都是非常耗費計算資源的。

在臨界區很短的情況下,可以考慮自旋鎖作為同步原語,也因為公共數據的讀取和修改是非常短暫的,但同時又是需要同步原語保護的臨界區,許多所謂的無鎖(lock-free)數據結構也是基於自旋鎖實現的,那麽什麽是自旋鎖呢?就是死循環檢查鎖是否處於未上鎖狀態,如果處於加鎖並且退出循環,這個過程可以有xchgb這個匯編語句來保證原子性。

那麽自旋鎖是如何實現的呢?下面用Linux的i386架構的源碼中自旋鎖作為例子說明三種數據結構的實現原理,三種數據結構分別是:自旋鎖(互斥鎖)讀寫鎖順序鎖

先說明xchgb這條匯編所代表的的含義:xchgb a, b,表達原子性的交換,即a復制給b,b復制給a。

自旋鎖

1 2 3 typedef struct { volatile unsigned int slock; /* 值1表示“未加鎖”,任何負數和0都表示“加鎖狀態” */ } spinlock_t;

見上面的註釋,slock值1表示“未加鎖”,任何負數和0都表示“加鎖狀態”,volatile是類型修飾符,確保本變量相關指令不會因編譯器的優化而省略,要求每次直接讀值(讀內存),防止編譯器優化掉。旋轉加鎖的過程如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /* 不阻塞的鎖,無法上鎖直接返回 */
inline int spin_trylock(spinlock_t *lock) { char oldval; __asm__ __volatile__( "xchgb %b0,%1" :"=q" (oldval), "=m" (lock->slock) :"0" (0) : "memory"); return oldval > 0; } /* 自旋鎖,未獲取鎖則進入死循環,直到獲取鎖為止 */ inline void spin_lock(spinlock_t *lock) { while(!spin_trylock(lock)){ ; } }

使用旋轉鎖的主要原因在於效率高,執行時間短(避免用戶態和內核態之間的切換),因此Linux選擇使用gcc編譯器的內聯匯編,__asm__ 是內聯匯編的關鍵字,__volatile__關鍵字就是讓gcc不要優化內存使用,如需語法見鏈接。

讀寫鎖

1 2 3 typedef struct { volatile unsigned int lock; } rwlock_t;

lock為0x01000000是標識“空”狀態,如果寫者已經獲得自旋鎖,那麽lock字段的值為0x00000000,如果一個,兩個或多個進程因為讀而獲得了自旋鎖,那麽lock字段上的值為0x00ffffff, 0x00fffffe等…讀鎖和寫鎖加鎖如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /* 寫鎖嘗試加鎖 */ inline int _raw_read_trylock(rwlock_t *lock) { atomic_t *count = (atomic_t *)lock; atomic_dec(count); if (atomic_read(count) >= 0) return 1; atomic_inc(count); return 0; } /* 讀鎖嘗試加鎖 */ inline int _raw_write_trylock(rwlock_t *lock) { atomic_t *count = (atomic_t *)lock; /* 如果計算結果為0,則返回1,否則返回0 */ if (atomic_sub_and_test(RW_LOCK_BIAS, count)) return 1; atomic_add(RW_LOCK_BIAS, count); return 0; }

順序鎖

讀寫鎖明顯有一個巨大的缺點,如果讀操作非常頻繁,寫鎖會有獲取不到鎖的可能,解決這個問題的辦法就是使用順序鎖,但壓下葫蘆起了瓢,它也有自己的缺點,讀者臨界區的代碼如下:

1 2 3 4 5 unsigned int seq;; do{ seq = read_seqbegin(&seqlock); /* 臨界區 */ } while(read_seqretry(&reqlock, seq));

可以看出來,讀者不得不反復多次讀相同的數據直到獲取有效的副本,但是仔細考量一下,在讀操作非常大量,寫操作基本沒有的情況下,這個鎖的效果也還是可以的。

不過還有一些限制條件:

  • 被保護的數據結構不包括間接指針指向的內容。
  • 讀者臨界代碼區沒有其他副本。

End

自旋鎖,讀寫鎖和順序鎖的實現原理