併發編程三條特性:
- 原子性 原子性是指一個操作是不可中斷的,要麼全部執行成功要麼全部執行失敗。
- 可見性 可見性是指當一個線程修改了共享變量後,其他線程能夠立即看見這個修改。
- 有序性 有序性是指程序指令按照預期的順序執行而非亂序執行,亂序又分爲編譯器亂序和CPU執行亂序。
1 volatile變量
volatile 變量不保證線程安全和不具備原子性的原因:在執行內存屏障之前,不同 CPU 依舊可以對同一個緩存行持有,一個 CPU 對同一個緩存行的修改不能讓另一個 CPU 及時感知,因此出現併發衝突。線程安全還是需要用鎖來保障,鎖能有效的讓 CPU 在同一個時刻獨佔某個緩存行,執行完並釋放鎖後,其他CPU才能訪問該緩存行。
2 原子變量
32位IA-32處理器使用基於對緩存加鎖或總線加鎖的方式來實現多處理器之間的原子操作。首先處理器會自動保證基本的內存操作的原子性。處理器保證從系統內存中讀取或寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其他處理器不能訪問這個字節的內存地址。兩張鎖機制如下:
- 使用總線鎖保證原子性 :總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔共享內存。
- 使用緩存鎖保證原子性 : 指內存區域如果被緩存在處理器的緩存行中,並且在Lock操作期間被鎖定,那麼當它執行鎖操作回寫到內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並允許它的緩存一致性機制來保證操作的原子性,因爲緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時,會使緩存行無效。
對於gcc、g++編譯器來講,它們提供了一組API來做原子操作:
type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)
bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_lock_test_and_set (type *ptr, type value, ...)
void __sync_lock_release (type *ptr, ...)
3 自旋鎖
底層是藉助cpu的原子指令CAS實現的,自旋鎖是採用忙等的狀態獲取鎖,所以會一直佔用cpu資源,但是允許不關閉中斷的情況下。同時因爲線程對cpu一直保持佔用狀態,所以對小資源加鎖效率比較高,不需要做任何的線程切換,一般情況下如果加鎖資源的運行延遲小於線程或者進程切換的時延則推薦使用自旋鎖。如果需要等待耗時操作,則建議採用互斥鎖。
//CAS操作在cpu指令集中可以是原子性的
int CompareAndExchange(int *ptr, int old, int new){
int actual = *ptr;
if (actual == old)
*ptr = new;
return actual;
}
void lock(lock_t *lock) {
while (CompareAndExchange(&lock->flag, 0, 1) == 1); // spin
}
void unlock(lock_t *lock) {
lock->flag = 0;
}
4 互斥鎖
互斥鎖更多的是強調對共享資源的鎖定作用,當一個線程佔用了當前共享資源,使用互斥鎖將其lock住之後,其他線程就無法訪問,必須等到unlock之後,其他線程才能利用共享資源裏面的內容;
- 互斥鎖是選擇睡眠的方式來對共享工作停止訪問的