多線程中的同步與鎖

多線程中的同步與鎖

概念

前面已經有介紹過線程的概念,因此咱們知道,當兩個線程同時讀寫一個內存區域的時候結果多是不肯定的.咱們假設寫操做須要兩個存儲器訪問週期,
而讀操做只須要一個訪問週期.在寫操做執行了一個訪問週期後讀操做開始執行,那麼獲得的結果可能並非咱們想要的.
在這個需求下,咱們就須要瞭解鎖以及同步的知識以更好的開發高性能的程序.多線程

互斥量(mutex)

  • 概念: 面對上面的需求咱們能夠經過互斥接口來保護數據,確保同一時間只有一個線程訪問數據.mutex本質上來講是一把鎖,咱們在訪問變量前進行
    加鎖,若是此時有任何線程試圖對相同的mutex執行加鎖操做都會被阻塞,直到完成操做後咱們對mutex解鎖.此時由於鎖被阻塞的線程會被喚醒.
  • 數據類型:互斥變量是用pthread_mutex_t數據類型表示的,下面是初始化以及銷燬互斥變量的函數原型函數

    #include<pthread.h>  
    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
                //參數attr設置爲NULL則爲默認屬性
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
                //兩個函數若成功則返回0,不然返回錯誤編號
  • 互斥量操做函數原型:性能

    #include<pthread.h>  
    int pthread_mutex_lock(pthread_t *mutex)  
    
    int pthread_mutex_trylock(pthread_t *mutex)  
    
    int pthread_mutex_unlock(pthread_t *mutex)  
                //全部函數若成功則返回0,不然返回錯誤編號
    • lock函數:對mutex變量進行加鎖操做,若是此時已有進程對此mutex加鎖則阻塞直到mutex被解鎖
    • trylock函數:若是不但願線程被阻塞,能夠調用trylock函數嘗試對mutex進行加鎖操做.若是此時mutex已加鎖,則返回EBUSY表示失敗.
    • unlock函數:對mutex變量進行解鎖操做.

超時互斥量

  • 假如咱們但願訪問一個受到保護的變量,可是又但願對這個訪問作出一個限制:若是等待5s仍舊沒法訪問咱們就放棄此次訪問.此時咱們就須要用到
    這個超時互斥量接口.
  • 函數原型:線程

    #include<time.h>
    #include<pthread.h>
    
    int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
                                const struct timespec *restrict tsptr);
                            //成功返回0,失敗則返回錯誤編號.
  • 在超時到來前mutex被解鎖,則函數行爲與lock一致.若是超時則返回ETIMEDOUT.rest

讀寫鎖

  • 假如咱們須要保護的變量被修改的次數遠遠小於被讀取的次數,此時咱們再使用mutex就會對性能形成一些浪費.由於在大量的讀操做中並不會形成
    亂序問題.在這種狀況下咱們就能夠利用讀寫鎖來減小加鎖形成的性能損失.code

  • 讀寫鎖經過結構體pthread_rwlock_t表示,不一樣於mutex,讀寫鎖在使用以前必須進行初始化,在釋放底層內存前必須銷燬.接口

    #include<pthread.h>
    
    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                            const pthread_rwlockattr_t *restrict attr);
    
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
                    //兩個函數成功則返回0,不然返回錯誤編號.
  • 函數原型:進程

    #include<pthread.h>
    
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
                    //全部函數成功則返回0,不然返回錯誤編號.
  • pthread_rwlock_rdlock:讀鎖,容許n(根據實現決定)個讀鎖同時鎖定.
  • pthread_rwlock_wrlock:寫鎖,當讀鎖存在時加寫鎖會形成進程阻塞.同時爲了不出現飢餓的狀況,寫鎖的狀態會阻塞住後面的讀鎖.
    也就是說,即便寫鎖是被阻塞的,一樣也會阻止其餘線程再添加讀鎖.
  • pthread_rwlock_unlock:不論以哪一種形式加鎖均可以經過unlock解鎖.若是但從函數表現來看,應該是鎖內部維護了狀態以及計數器,若是是
    讀鎖則將鎖數量減1,若是是寫鎖則切換鎖狀態(Go語言的實現方法,我並無查過C語言的實現源碼)內存

  • 讀寫鎖原語:資源

    #include<pthread.h>
    
    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
    
    int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
                        //兩個函數成功則返回0,不然返回錯誤編號.

    行爲等同於mutex的鎖原語.

條件變量

  • 當咱們的需求不只僅是鎖住某個臨界區,而且還須要判斷某些條件是否成立,這個時候條件變量是比mutex更好的選擇.

  • 爲何要用條件變量:
    咱們來假設這樣一個場景,四個線程讀取緩衝區,兩個線程寫入緩衝區.假如此時緩衝區爲空,而且寫入線程阻塞等待數據,這種狀況下四個讀取線程會作什麼呢?它們會不停的循環進行加鎖-判斷緩衝區內容-緩衝區爲空-解鎖.
    這樣的頻繁加鎖解鎖的操做很大程度上浪費了CPU資源,因此此時咱們須要引入條件變量來幫助咱們解決這一問題.

  • 初始化:

    #include<pthread.h>
    
    int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
    
    int pthread_cond_destroy(pthread_cond_t *restrict cond);
    
                //成功則返回0,失敗則返回錯誤代碼
    • 使用條件變量前必須進行初始化.
  • wait:

    #include<pthread.h>
    
    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
    
    int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex,
                                const struct timespec *restrict tsptr);
    
                                //成功則返回0,失敗則返回錯誤代碼
    • wait利用mutex以及條件變量對線程進行阻塞,調用者將鎖住的互斥量傳遞給函數,函數將線程放在等待條件的線程列表上並對互斥量解鎖.這樣線程就不會錯過任何條件變化.當函數返回時,該條件再次
      被加鎖.

    • timedwait對wait增長了超時限制(tsptr參數).

  • 喚醒:

    #include<pthread.h>
    
    int pthread_cond_signal(pthread_cond_t *cond);
    
    int pthread_cond_broadcast(pthread_cond_t *cond);
    
                                //成功則返回0,失敗則返回錯誤代碼
    • 經過這兩個函數喚醒等待條件的進程,signal至少能喚醒一個,而broadcast則能喚醒所有等待條件的進程.

自旋鎖

  • 自旋鎖與mutex相似,都是經過阻塞的形式阻止得到已被加鎖的鎖.

  • mutex被阻塞時直接陷入睡眠等待信號(sleep-waiting),而自旋鎖則是忙等待(busy-waiting),也就是說自旋鎖的等待是佔用CPU的

  • 自旋鎖初始化:

    #include<pthread.h>
    
    int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
    
    int pthread_spin_destroy(pthread_spinlock_t *lock);
                //兩個函數若成功則返回0,不然返回錯誤編號
    • pshared參數:設置爲PTHREAD_PROCESS_SHARED則能夠被不一樣的進程共享,PTHREAD_PROCESS_PRIVATE只能被初始化鎖的內部線程所訪問
  • 函數原型:

    #include<pthread.h>
    
    int pthread_spin_lock(pthread_spinlock_t *lock);
    
    int pthread_spin_trylock(pthread_spinlock_t *lock);
    
    int pthread_spin_unlock(pthread_spinlock_t *lock);
    
                //全部函數若是成功則返回0,失敗返回錯誤編號
  • 須要注意的是,使用自旋鎖的範圍其實很窄,除非頻繁切換線程的開銷很是大或者咱們持有鎖的時間很是短,不然使用自旋鎖對cpu的佔用其實很高.

屏障

  • 屏障是用戶協調多個線程並行工做的同步機制.屏障容許每一個線程等待,直到全部的線程都到達某一點(和go裏面的WaitGroup同樣).

  • 初始化:

    #include<pthread.h>
    
    int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr,unsigned int count);
    
    int pthread_barrier_destroy(pthread_barrier_t *barrier);
                //兩個函數若成功則返回0,不然返回錯誤編號
  • 函數原型:

    #include<pthread.h>
    
    int pthread_barrier_wait(pthread_barrier_t *barrier);
            //成功則返回0或者PTHREAD_BARRIER_SEGIAL_THREAD,不然返回錯誤編號
  • 調用wait的函數會在條件不知足時阻塞,直到count計數知足以後全部線程被喚醒.

  • 須要注意的是,到達屏障計數後屏障能夠被重置,但此時的屏障計數沒有改變.因此咱們想要重用須要destroy而後再init初始化.

相關文章
相關標籤/搜索