Linux編程學習筆記 | Linux多線程學習[2] - 線程的同步

什麼是線程的同步

當有多個線程要同時對一個共享的內存空間進行讀寫時,咱們要保證這個內存空間對於多個線程來講是一致的。當多個線程同時讀/寫這個內存空間時,就須要對線程進行同步,以確保任什麼時候刻只有一個線程能修改該內存空間,這樣才能保證線程不會訪問到無效的數據。
我經過下面這幅圖解釋下線程同步的重要性:多線程

線程同步的重要性

在這個例子中,兩個線程A和B都要按順序作如下3件事:函數

  1. 將變量 i 寫入寄存器spa

  2. 寄存器加1線程

  3. 將寄存器內容從新寫回變量 i指針

線程A先運行,線程B在線程A運行到第2步時開始運行,咱們期待的結果是最終變量 i 的值會加2,但因爲這兩個線程沒有進行同步,最後變量 i 的值只加了1。所以,對於多線程程序來講,線程的同步是很重要的。rest

線程的同步既然這麼重要,那咱們能經過什麼辦法來對其進行同步呢?我這裏介紹三種基本的線程同步方法:code

  1. 互斥量(mutex)內存

  2. 讀寫鎖(rwlock)資源

  3. 條件變量(cond)rem

互斥量

簡單來講,互斥量就是一把鎖住共享內存空間的鎖,有了它,同一時刻只有一個線程能夠訪問該內存空間。當一個線程鎖住內存空間的互斥量後,其餘線程就不能訪問這個內存空間,直到鎖住該互斥量的線程解開這個鎖。

互斥量的初始化

對於一個互斥量,咱們首先須要對它進行初始化,而後才能將其鎖住和解鎖。咱們可使用動態分配和靜態分配兩種方式初始化互斥量。

分配方式 說明
動態分配 調用pthread_mutex_init()函數,在釋放互斥量內存空間前要調用pthread_mutex_destroy()函數
靜態分配 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

下面是 pthread_mutex_init()pthread_mutex_lock() 函數的原型:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                       const pthread_mutexattr_t *restrict attr);
                       
args:
    pthread_mutex_t *restrict mutex         : 指向須要被初始化的互斥量的指針
    const pthread_mutexattr_t *restrict attr: 指向須要被初始化的互斥量的屬性的指針

return:
    互斥量初始化的狀態,0是成功,非0是失敗
    


int pthread_mutex_destroy(pthread_mutex_t *mutex);

args:
    pthread_mutex_t *mutex: 指向須要被銷燬的互斥量的指針
    
return:
    互斥量銷燬的狀態,0是成功,非0是失敗

互斥量的操做

互斥量的基本操做有三種:

互斥量操做方式 說明
pthread_mutex_lock() 鎖住互斥量,若是互斥量已經被鎖住,那麼會致使該線程阻塞。
pthread_mutex_trylock() 鎖住互斥量,若是互斥量已經被鎖住,不會致使線程阻塞。
pthread_mutex_unlock() 解鎖互斥量,若是一個互斥量沒有被鎖住,那麼解鎖就會出錯。

上面三個函數的原型:

int pthread_mutex_lock(pthread_mutex_t *mutex);

args:
    pthread_mutex_t *mutex: 指向須要被鎖住的互斥量的指針
    
return:
    互斥量鎖住的狀態,0是成功,非0是失敗
    


int pthread_mutex_trylock(pthread_mutex_t *mutex);

args:
    pthread_mutex_t *mutex: 指向須要被鎖住的互斥量的指針
return:
    互斥量鎖住的狀態,0是成功,非0是失敗
    


int pthread_mutex_unlock(pthread_mutex_t *mutex);

args:
    pthread_mutex_t *mutex: 指向須要被解鎖的互斥量的指針
    
return:
    互斥量解鎖的狀態,0是成功,非0是失敗

死鎖

若是互斥量使用不當可能會形成死鎖現象。死鎖指的是兩個或兩個以上的線程在執行過程當中,因爭奪資源而形成的一種互相等待的現象。好比線程1鎖住了資源A,線程2鎖住了資源B;咱們再讓線程1去鎖住資源B,線程2去鎖住資源A。由於資源A和B已經被線程1和2鎖住了,因此線程1和2都會被阻塞,他們會永遠在等待對方資源的釋放。

爲了不死鎖的發生,咱們應該注意如下幾點;

  1. 訪問共享資源時須要加鎖

  2. 互斥量使用完以後須要銷燬

  3. 加鎖以後必定要解鎖

  4. 互斥量加鎖的範圍要小

  5. 互斥量的數量應該少

讀寫鎖

讀寫鎖和互斥量類似,不過具備更高的並行性。互斥量只有鎖住和解鎖兩種狀態,而讀寫鎖能夠設置讀加鎖,寫加鎖和不加鎖三種狀態。對於寫加鎖狀態而言,任什麼時候刻只能有一個線程佔有寫加鎖狀態的讀寫鎖;而對於讀加鎖狀態而言,任什麼時候刻能夠有多個線程擁有讀加鎖狀態的讀寫鎖。下面是一些讀寫鎖的特性:

特性 說明
1 當讀寫鎖是寫加鎖狀態時,在這個鎖被解鎖以前,全部試圖對這個鎖加鎖的線程都會被阻塞。
2 當讀寫鎖是讀加鎖狀態時,全部處於讀加鎖狀態的線程均可以對其進行加鎖。
3 當讀寫鎖是讀加鎖狀態時,全部處於寫加鎖狀態的線程都必須阻塞直到全部的線程釋放該鎖。
4 當讀寫鎖是讀加鎖狀態時,若是有線程試圖以寫模式對其加鎖,那麼讀寫鎖會阻塞隨後的讀模式鎖請求。

讀寫鎖的初始化

同互斥量相似,咱們須要先初始化讀寫鎖,而後才能將其鎖住和解鎖。要初始化讀寫鎖,咱們使用 pthread_rwlock_init() 函數,同互斥量相似,在釋放讀寫鎖內存空間前,咱們須要調用 pthread_rwlock_destroy() 函數來銷燬讀寫鎖。

下面是 pthread_rwlock_init()pthread_rwlock_destroy() 函數的原型:

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                        const pthread_rwlockattr_t *restrict attr);
                        
args:
    pthread_rwlock_t *restrict rwlock:          指向須要初始化的讀寫鎖的指針
    const pthread_rwlockattr_t *restrict attr: 指向須要初始化的讀寫鎖屬性的指針 

return:
    讀寫鎖初始化的狀態,0是成功,非0是失敗

    

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向須要被銷燬的讀寫鎖的指針

return:
    讀寫鎖銷燬的狀態,0是成功,非0是失敗

讀寫鎖的操做

同互斥量相似,讀寫鎖的操做也分爲阻塞和非阻塞,咱們先來看看讀寫鎖有哪些基本操做:

讀寫鎖操做方式 說明
int pthread_rwlock_rdlock() 讀寫鎖讀加鎖,會阻塞其餘線程
int pthread_rwlock_tryrdlock() 讀寫鎖讀加鎖,不阻塞其餘線程
int pthread_rwlock_wrlock() 讀寫鎖寫加鎖,會阻塞其餘線程
int pthread_rwlock_trywrlock() 讀寫鎖寫加鎖,不阻塞其餘線程
int pthread_rwlock_unlock() 讀寫鎖解鎖

下面是這幾個函數的原型:

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向須要加鎖的讀寫鎖的指針

return:
    讀寫鎖加鎖的狀態,0是成功,非0是失敗



int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向須要加鎖的讀寫鎖的指針

return:
    讀寫鎖加鎖的狀態,0是成功,非0是失敗



int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向須要加鎖的讀寫鎖的指針 

return:
    讀寫鎖加鎖的狀態,0是成功,非0是失敗



int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向須要加鎖的讀寫鎖的指針 

return:
    讀寫鎖加鎖的狀態,0是成功,非0是失敗
    


int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向須要解鎖的讀寫鎖的指針 

return:
    讀寫鎖解鎖的狀態,0是成功,非0是失敗

條件變量

條件變量是和互斥量一塊兒使用的。若是一個線程被互斥量鎖住,但這個線程卻不能作任何事情時,咱們應該釋放互斥量,讓其餘線程工做,在這種狀況下,咱們可使用條件變量;若是某個線程須要等待系統處於某種狀態才能運行,此時,咱們也可使用條件變量。

條件變量的初始化

同互斥量同樣,條件變量可使用動態分配和靜態分配的方式進行初始化:

分配方式 說明
動態分配 調用pthread_cond_init()函數,在釋放條件變量內存空間前須要調用pthread_cond_destroy()函數
靜態分配 pthread_cond_t cond = PTHREAD_COND_INITIALIZER

下面是 pthread_cond_init()pthread_cond_destroy() 函數的原型:

int pthread_cond_init(pthread_cond_t *restrict cond,
                      const pthread_condattr_t *restrict attr);
                      
args:
    pthread_cond_t *restrict cond          : 指向須要初始化的條件變量的指針
    const pthread_condattr_t *restrict attr: 指向須要初始化的條件變量屬性的指針
    
return:
    條件變量初始化的狀態,0是成功,非0是失敗



int pthread_cond_destroy(pthread_cond_t *cond);

args:
    pthread_cond_t *cond: 指向須要被銷燬的條件變量的指針

return:
    條件變量銷燬的狀態,0是成功,非0是失敗

條件變量的操做

條件變量的操做分爲等待和喚醒,等待操做的函數有 pthread_cond_wait()pthread_cond_timedwait() ;喚醒操做的函數有 pthread_cond_signal()pthread_cond_broadcast()

咱們來看看 pthread_cond_wait() 是怎麼使用的,下面是函數原型:

int pthread_cond_wait(pthread_cond_t *restrict cond,
                      pthread_mutex_t *restrict mutex);
                      
args:
    pthread_cond_t *restrict cond  : 指向須要等待的條件變量的指針
    pthread_mutex_t *restrict mutex: 指向傳入互斥量的指針

return:
    0是成功,非0是失敗

當一個線程調用 pthread_cond_wait() 時,須要傳入條件變量和互斥量,這個互斥量必需要是被鎖住的。當傳入這兩個參數後,

  1. 該線程將被放到等待條件的線程列表中

  2. 互斥量被解鎖

這兩個操做都是原子操做。當這兩個操做結束後,其餘線程就能夠工做了。當條件變量爲真時,系統切換回這個線程,函數返回,互斥量從新被加鎖。

當咱們須要喚醒等待的線程時,咱們須要調用線程的喚醒函數,下面是函數的原型:

int pthread_cond_signal(pthread_cond_t *cond);

args:
    pthread_cond_t *cond: 指向須要喚醒的條件變量的指針
    
return:
    0是成功,非0是失敗


    
int pthread_cond_broadcast(pthread_cond_t *cond);
 
args:
    pthread_cond_t *cond: 指向須要喚醒的條件變量的指針 
    
return:
    0是成功,非0是失敗

pthread_cond_signal()pthread_cond_broadcast() 的區別在於前者用於喚醒一個等待條件的線程,然後者用於喚醒全部等待條件的線程。

總結

這篇文章主要介紹了多線程中同步的重要性和線程同步的三種方法。在下篇文章中我將經過程序實例來演示如何在代碼中使用多線程。

若是以爲本文對你有幫助,請多多點贊支持,謝謝!

相關文章
相關標籤/搜索