當多個線程共享相同的內存時,需要確保每個線程看到一致的數據視圖,當多個線程同時去修改這片內存時,就可能出現偏差,得到與預期不符合的值。為啥需要同步,一件事情邏輯上一定是有序的,即使在並發環境下;而操作系統對於多線程不會自動幫我們串行化,所以需要我們通過操作系統提供的同步方式api,結合自己的業務邏輯,利用多線程提高性能的同時,保證業務邏輯的正確性。一般而言,linux下同步方式主要有4種,原子鎖,互斥量,讀寫鎖和條件變量。下面一一介紹幾種同步方式。
1. spinlock
1) 概念
spinlock是一種互斥結構,通過CPU提供的特殊的原子指令集合實現互斥地訪問一個資源,需要硬件支持。一個線程訪問數據未結束的時候,其他線程不得對同一個數據進行訪問。
2) 實現
spinlock一般基於原子的read-modify-write操作實現。read-modify-write操作允許一個CPU讀取一個值,修改該值,並將修改完成的值回寫內存的三個操作作為一個原子總線操作,因此需要CPU特殊支持。具體而言,通過test-and-set指令實現,從內存中讀取一個值,然後和0比較,並且將內存中的值設置為1。
3) 相關函數
a) 原子操作
1 test_and_set(volatile int* addr, value)
2 {
3 return os_atomic_test_and_set_int(addr,value);
4 }
這裡volatile修飾詞告訴編譯器從內存中獲取,保證正確性,避免從寄存器中讀取到不准確的值。
b) 設置鎖變量
1 set_spinlock(lock_word)
2 {
3 int i = 0;
4 int value;
5 while (true)
6 {
7 value = test_and_set(&lock_word, 1);
8 if (value == 0) //尚未被占用,可以獲取
9 break;
10 else //已經被其他線程占用,繼續輪轉
11 do nothing
12 }
13 }
c) 重置鎖變量
1 reset_spinlock(lock_word)
2 {
3 test_and_set (lock_word, 0);
4 }
2. mutex
1) 概念
與 spinlock作用相同,保證互斥地訪問一個資源。
2) 相關函數
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經被占據時返回EBUSY而不是掛起等待。
3) spinlock與mutex的區別
a) 作用范圍,mutex是內核對象,可以在多線程,多進程同步中使用;spinlock作用范圍僅限於本進程(鎖變量是進程內的),僅適用於多線程同步。
b) spinlock依賴於硬件的原子操作指令
c) 線程獲取spinlock失敗時,會采取循環等待的方式,此時線程處於運行狀態,CPU空轉;而獲取mutex失敗時,線程會掛起,線程處於wait狀態,不會被內核調度。
d) 由於3的特點,進入等待狀態或從等待狀態被喚醒,都涉及到CPU的上下文切換,而CPU切換是比較耗時的,一般需要25us。相對而言spinlock則沒有這樣的代價,效率更高。
e) 也由於3的特點,spinlock會空轉,導致浪費大量的CPU時間片,若用戶持有鎖時間長,導致空轉時間長,也得不償失。因此spinlock比較適合於“快拿快放”的使用場景。
3.讀寫鎖
1) 概念
spinlock和互斥量都是保證同一時刻只有一個線程操作共享內存。互斥鎖要麼是加鎖狀態,要麼是不加鎖狀態,一次只有一個線程可以對其加鎖。讀寫所可以有3種狀態,讀模式下加鎖狀態,寫模式下加鎖狀態,不加鎖狀態。相對於前兩者,讀寫鎖有更高的並發度,允許多個線程同時讀共享內存。
2) 相關函數
pthread_rwlock_rdlock(pthread_rwlock_t*); 讀鎖定
pthread_rwlock_tryrdlock(pthread_rwlock_t*); 非阻塞讀鎖定
pthread_rwlock_wrlock(pthread_rwlock_t*); 寫鎖定
pthread_rwlock_trywrlock(pthread_rwlock_t*); 非阻塞寫鎖定
pthread_rwlock_unlock(pthread_rwlock_t*); 釋放鎖
4. 條件變量
1) 概念
條件變量是另外一種同步機制,通過與互斥鎖配合使用,利用鎖保護條件變量,通過條件變量實現喚醒和等待的機制。通過這種方式,允許線程以無競爭的方式等待特定的條件發生
2) 相關函數
int pthread_cond_signal(pthread_cond_t *cond); //喚醒等待條件某個線程
int pthread_cond_broadcast(pthread_cond_t *cond); //喚醒等待條件所有線程
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); //等待條件發生。
3) 說明
調用pthread_cond_wait之前,需要線程獲取互斥量,調用者把互斥量傳遞給函數,函數把調用線程發到等待隊列上,然後對互斥量解鎖,這個操作是原子操作。當pthread_cond_wait返回時,互斥量會再次被鎖住,這個實現都在pthread_cond_wait函數中實現,不需要用戶邏輯介入。