成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

多線程讀寫鎖產(chǎn)生死鎖的故障解決方案

開發(fā) 系統(tǒng)
本文從一次協(xié)程泄露問題入手,分析golang讀寫鎖可能產(chǎn)生死鎖的場景,希望讀者可以避坑。

作者 | morphis

多線程環(huán)境下,讀寫鎖是一種常用的同步原語,適用于多讀者-多寫者的經(jīng)典問題;合理的使用可以在保證數(shù)據(jù)一致性的前提下,大幅提升讀性能,但不合理的使用可能會導(dǎo)致死鎖。本文從一次協(xié)程泄露問題入手,分析golang讀寫鎖可能產(chǎn)生死鎖的場景,希望讀者可以避坑。

一、故障背景

近期線上某個trpc-go服務(wù)一直在OOM,據(jù)以往查障經(jīng)驗,golang服務(wù)發(fā)生內(nèi)存持續(xù)上漲大概率是由兩個原因?qū)е?

  • 請求量過大,服務(wù)處理不過來,造成協(xié)程積壓,或者資源積壓;
  • 協(xié)程泄露,由于未正確關(guān)閉、或者協(xié)程阻塞等原因,導(dǎo)致協(xié)程積壓。

123平臺容器監(jiān)控如下:

二、排查思路

1. 檢查請求量級

第一時間排查了一下服務(wù)的請求量和CPU/內(nèi)存情況,發(fā)現(xiàn)請求量并未上漲,內(nèi)存上漲的同時,CPU并未線性相關(guān)上漲,但是協(xié)程數(shù)一直在上漲;這里就排除了請求積壓這個原因。

2. 檢查協(xié)程泄露

在請求量并未明顯上漲的前提下,協(xié)程數(shù)在上漲,比較符合協(xié)程泄露的現(xiàn)象;于是借助123平臺的pprof工具,采樣觀測了一下協(xié)程積壓情況,如下:

發(fā)現(xiàn)協(xié)程大量積壓在gopark函數(shù)等待上,證明大量協(xié)程正掛起等待,根據(jù)調(diào)用棧定位到業(yè)務(wù)代碼分別是對一個讀寫鎖的讀鎖、寫鎖加鎖行為:

(*RWMutex).Rock()
(*RWMutex).Lock()

3. 定位協(xié)程泄漏點

定位到這里其實已經(jīng)基本猜到是由于讀寫鎖的阻塞導(dǎo)致協(xié)程泄露,遂review代碼,兩個競態(tài)的協(xié)程分別按順序執(zhí)行的加鎖行為如下:

(1)協(xié)程A:讀鎖-加鎖,第一次。

(2)協(xié)程A:讀鎖-加鎖,第二次:

(3)協(xié)程B:寫鎖-加鎖

回想操作系統(tǒng)原理中,死鎖產(chǎn)生的4個必要條件:

  • 互斥(Mutual Exclusion):至少有一個資源必須處于非共享模式,也就是說,在一段時間內(nèi)只能有一個進程使用該資源。如果其他進程請求該資源,請求者只能等待,直到資源被釋放。
  • 占有并等待(Hold and Wait):一個進程至少持有一個資源,并且正在等待獲取額外的資源,這些額外資源被其他進程持有。非搶占(No Preemption):資源不能被強制從一個進程中搶占,只能由持有資源的進程主動釋放。
  • 循環(huán)等待(Circular Wait):必須存在一個進程—資源的環(huán)形鏈,其中每個進程至少持有一個資源,但又等待另一個被鏈中下一個進程持有的資源。

互斥、非搶占、占有并等待在現(xiàn)代編程語言中的同步語法中都是容易滿足的,問題就出在循環(huán)等待上,在讀寫鎖的使用上,經(jīng)常出現(xiàn)死鎖的情況是:

協(xié)程A:讀鎖 - 加鎖 協(xié)程A:寫鎖 - 加鎖

一般是同一個協(xié)程在持有鎖(讀/寫)的情況下,請求寫鎖,這個時候?qū)戞i因為持有的鎖并未釋放,而永久等待,陷入死鎖困境;回過頭去review代碼,并未出現(xiàn)這種情況,但是RLock重入了一次,仔細閱讀了golang官方文檔,禁止RLock的遞歸調(diào)用,換句話說是在同一協(xié)程下,讀鎖未UnLock的情況下,禁止重復(fù)調(diào)用RLock(當(dāng)然也不能調(diào)用Lock),否則可能導(dǎo)致死鎖:

A RWMutex must not be copied after first use.

If any goroutine calls RWMutex.Lock while the lock is already held by one or more readers, concurrent calls to RWMutex.RLock will block until the writer has acquired (and released) the lock, to ensure that the lock eventually becomes available to the writer. Note that this prohibits recursive read-locking.

排查至此,問題已經(jīng)明確,是有重入讀鎖導(dǎo)致的死鎖,進而引發(fā)的協(xié)程泄露。

三、讀寫鎖原理

1. 讀寫鎖適用場景

深究死鎖產(chǎn)生的原因,不得不探究一下讀寫鎖的適用場景。在大多數(shù)編程語言中,如果存在多線程對數(shù)據(jù)的競爭,最常用的一個方案是數(shù)據(jù)加互斥鎖/排它鎖(Mutex),互聯(lián)網(wǎng)業(yè)務(wù)特點,在對數(shù)據(jù)的操作上,普遍是讀遠多于寫,如果所有對數(shù)據(jù)的操作都互斥,即便是沒有寫行為,大量并發(fā)的讀操作也會退化成成串行訪問。這個時候就需要差異化對待讀、寫行為,使用更‘細粒度’的鎖-讀寫鎖(共享鎖),基本特性概括如下:

  • 讀鎖和讀鎖 - 不互斥
  • 讀鎖和寫鎖 - 互斥
  • 寫鎖和寫鎖 - 互斥

這樣就可以在持有寫鎖時,任何讀/寫行為都會被阻塞;且未持有寫鎖時,任何讀行為都不會被阻塞;即保護了數(shù)據(jù),又保證了性能;聽起來很“完美”的解決方案,但經(jīng)常寫技術(shù)方案的同學(xué)都知道,沒有最好的技術(shù)方案,只有合適的技術(shù)方案。既然根據(jù)讀、寫操作對鎖進行了區(qū)分,那么針對不同的業(yè)務(wù)場景(讀密集 or 寫密集),不同的設(shè)計取向(性能優(yōu)先 or 一致性優(yōu)先),必然面臨一個問題:讀寫鎖獲取鎖的優(yōu)先級是怎樣的?

2. 優(yōu)先級策略

針對reader-writer問題,基于對讀和寫操作的優(yōu)先級,讀寫鎖的設(shè)計和實現(xiàn)也分成三類:

  • Read-preferring: 讀優(yōu)先策略,可實現(xiàn)最大并發(fā)性,但如果讀操作密集,會導(dǎo)致寫鎖饑餓。因為只要一個讀取線程持有鎖,寫入線程就無法獲取鎖。如果有源源不斷的讀操作,寫鎖只能等待所有讀鎖釋放后才能獲取到。
  • Writer-preferring:寫優(yōu)先的策略,可以保證即便在讀密集的場景下,寫鎖也不會饑餓;只要有一個寫鎖申請加鎖,那么就會阻塞后續(xù)的所有讀鎖加鎖行為(已經(jīng)獲取到讀鎖的reader不受影響,寫鎖仍然要等待這些讀鎖釋放之后才能加鎖)。
  • Unspecified(不指定):不區(qū)分reader和writer優(yōu)先級,中庸之道,讀寫性能不是最優(yōu),但是可以避免饑餓問題。

3. 源碼分析

golang標(biāo)準(zhǔn)庫里讀寫鎖的實現(xiàn)(RWMutex),采用了writer-preferring的策略,使用Mutex實現(xiàn)寫-寫互斥,通過信號量等待和喚醒實現(xiàn)讀-寫互斥,RWMutex定義如下:

type RWMutex struct {
  w           Mutex        // 互斥鎖,用于寫鎖互斥
  writerSem   uint32       // writer信號量,讀鎖RUnlock時釋放,可以喚醒等待寫加鎖的線程
  readerSem   uint32       // reader信號量,寫鎖Unlock時釋放,可以喚醒等待讀加鎖的線程
  readerCount atomic.Int32 // 所有reader的數(shù)量(包括等待讀鎖和已經(jīng)獲得讀鎖)
  readerWait  atomic.Int32 // 已經(jīng)獲取到讀鎖的reader數(shù)量,writer需要等待這個變量歸0后才可以獲得寫鎖
}

(1)讀鎖-加鎖

readerCount是一個有多重含義的變量,在沒有寫鎖的情況下,每次讀鎖加鎖,都會原子+1;每次讀鎖釋放,readerCount會原子-1,所以在沒有寫鎖的情況下readerCount=獲取到的讀鎖的reader數(shù)量;但是當(dāng)存寫鎖pending時,都會將readerCount置為負數(shù),所以這里判斷為負數(shù)時,直接進入信號量等待。

func (rw *RWMutex) RLock() {
        // ...
 if rw.readerCount.Add(1) < 0 {
               // 當(dāng)有writer在pending時,readerCount會被加一個很大的負數(shù),保證readerCount變成負數(shù)
               // 有writer在等待鎖,需要等待reader信號量
  runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
 }
 // ...
}

(2)讀鎖-釋放

reader釋放讀鎖時,優(yōu)先將readerCount-1,這時如果還是負數(shù),證明有寫鎖在pending,這個時候需要釋放信號量,以便喚醒等待寫鎖的writer;當(dāng)然,前提需要所有已經(jīng)獲得讀鎖的reader都釋放讀鎖后(readerWait == 0),那問題來了,為什么需要先檢查readerCount,再對readerWait-1,實際上readerWait只有在有寫鎖在pending時才會生效,否則,readerCount就等于已經(jīng)獲得讀鎖的reader數(shù)量。

func (rw *RWMutex) RUnlock() {
        // ...
        if r := rw.readerCount.Add(-1); r < 0 {
  // 這里將相對slow的代碼封裝起來,以便RUnlock()可以被更多場景內(nèi)聯(lián),提升程序性能
                // Outlined slow-path to allow the fast-path to be inlined
  rw.rUnlockSlow(r)
 }
 // ...
}
func (rw *RWMutex) rUnlockSlow(r int32) {
 // ...
 // readerWait變量,記錄真正獲得讀鎖的reader數(shù)量,當(dāng)這個變量規(guī)0時,需要釋放信號量,以便喚醒等到寫鎖的writer
 if rw.readerWait.Add(-1) == 0 {
  // The last reader unblocks the writer.
  runtime_Semrelease(&rw.writerSem, false, 1)
 }
}

(3)寫鎖-加鎖

寫鎖間的互斥需要依賴互斥量,所以首先需要對w進行競爭加鎖;當(dāng)獲取到w之后,證明可以獨占寫操作;這個時候再來檢查讀鎖的情況;這里不得不感慨golang底層庫的實現(xiàn)之精妙,在一個計數(shù)值上賦予了多重含義,將readerCount加一個巨大的、固定的負數(shù)可以保證readerCount為負數(shù),這樣就可以在標(biāo)記有一個寫鎖在pending的同時,也不會丟失readerCount的數(shù)量。

func (rw *RWMutex) Lock() {
 // ...
 rw.w.Lock() // 寫鎖互斥
 // 將readerCount加一個巨大的、固定的負數(shù),保證readerCount為負數(shù)
        // r代表readerCount變成負數(shù)的那一刻的readCount,代表了請求寫鎖那一刻'注定'能獲取讀鎖的reader數(shù)量,在此之后的reader不管怎么對readerCount+1,都會阻塞到信號量等待上;
        // 所以r的值就是在加寫鎖的那一刻,已經(jīng)獲得讀鎖的reader數(shù)量
 r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
 // 當(dāng)有reader已經(jīng)獲得讀鎖時,需要等待信號量
 if r != 0 && rw.readerWait.Add(r) != 0 {
  runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
 }
 // ...
}

r代表著在寫鎖加鎖的那一刻(是一個瞬時值,可能會變),已經(jīng)獲得讀鎖的reader數(shù)量,通過過對readerWait賦值和判斷,決定是否需要等待信號量;那么問題來了,既然r是一個瞬時值,如果r已經(jīng)變了,怎么保證readerWait是準(zhǔn)的,例如:

在執(zhí)行這行代碼時,r=5,代表有5個reader獲得了讀鎖,此時readerWait==0:

r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders

而在執(zhí)行下面這行之前,有2個reader已經(jīng)釋放讀鎖,此時readerWait==-2,再執(zhí)行下面這樣代碼后,readerWait==3,這樣設(shè)計的精妙之處就在于,不管readerWait中間如何變化,只要在使用的那一刻他是最終準(zhǔn)確的就可以,所以嚴格意義上講readerWait記錄的是已經(jīng)持有讀鎖的reader數(shù)量,或者"自有寫鎖pending那一刻來,被釋放的讀鎖的負數(shù)量"

if r != 0 && rw.readerWait.Add(r) != 0 {

這也是在讀鎖釋放時,必須要判斷readerWait==0,而不是<=0的原因:

if rw.readerWait.Add(-1) == 0 {

(4)寫鎖-釋放

寫鎖的釋放主要做3件事,將readerCount置為正數(shù),表示新的reader可以不用等待信號量,直接獲取讀鎖了;對正在阻塞等待信號量的reader,依次喚醒;注意,這里的r也是一個瞬時值,代表著寫鎖釋放那一刻,正在等待信號量的reader,之后不管新的reader如何對readerCount+1,writer需要喚醒的reader也停留在“寫鎖釋放那一刻”;最后釋放互斥量,允許其他writer獲取寫鎖。

func (rw *RWMutex) Unlock() {
 // ...
 // 恢復(fù)readerCount為正數(shù),表示reader可以獲取讀鎖了
 r := rw.readerCount.Add(rwmutexMaxReaders)
 // 省略...
 // 釋放信號量,喚醒等待的reader
 for i := 0; i < int(r); i++ {
  runtime_Semrelease(&rw.readerSem, false, 0)
 }
 // 釋放互斥量,允許其他寫鎖獲取
 rw.w.Unlock()
 // ...
}

golang讀寫鎖的實現(xiàn),可以保證在沒有writer請求寫鎖時,讀操作可以保證最大的性能,此時的讀鎖加鎖-釋放行為,開銷非常小,僅僅是原子更新readerCount;當(dāng)有writer請求寫鎖時,寫鎖的加鎖-釋放行為會重一些,已經(jīng)獲得讀鎖的reader也不受影響;后續(xù)再請求讀鎖的reader將被阻塞,直到寫鎖釋放,大道至簡。

四、問題總結(jié)

1. 故障原因

回到故障本身,死鎖的原因是讀鎖的重入,造成了死鎖;實際上單純的讀鎖重入,不會造成死鎖;造成死鎖的case時序可以表示如下:

  • reader0:加鎖R0成功
  • writer0:  加寫鎖W0申請,標(biāo)記已經(jīng)有writer等待寫鎖,然后等待R0釋放,W0->R0
  • reader0:  嘗試加鎖R1,發(fā)現(xiàn)已經(jīng)有writer在等待,因為寫鎖優(yōu)先級較高,所以需要等待W0釋放,R1->W0
  • reader0:  由于代碼邏輯上R0,R1是先后關(guān)系,所以R0需要依賴R1釋放,R0->R1

這樣就造成了死鎖必要的循環(huán)依賴條件:R0->R1->W0->R0

2. 解決方案

明確了故障原因,解決方案就顯而易見了,避免重復(fù)調(diào)用鎖就可以避免此類死鎖的發(fā)生,但函數(shù)調(diào)用靈活復(fù)雜的,無法避免函數(shù)的嵌套調(diào)用,目前也沒有效的手段可以在編譯期發(fā)現(xiàn)這個問題;但我們可以通過一些范式來盡量避免,總結(jié)而言就是-臨界區(qū)一定要小:

  • 鎖的粒度一定要小,一方面避免粒度過大造成鎖忘記釋放,另一方面避免臨界區(qū)過大造成資源浪費。
  • 避免在臨界區(qū)嵌套調(diào)用函數(shù),一般情況下,需要加鎖的情況無外乎對數(shù)據(jù)的讀和寫操作,應(yīng)當(dāng)在一眼所視范圍內(nèi)對數(shù)據(jù)進行讀寫操作,而不是通過調(diào)用函數(shù)的方式。
  • defer釋放鎖要格外小心,defer可以方便的釋放鎖,可以避免忘記釋放鎖,可能第一版代碼臨界區(qū)就幾行,隨著后續(xù)維護者的拓展,臨界區(qū)變成了幾十行、上百行,這個時候其實就需要review一下,鎖的保護范圍到底是誰,是否粒度過大;若粒度過大,就應(yīng)該棄用defer,主動盡早釋放鎖。

3. 拓展思考

(1) golang

在排查故障的過程中,了解到golang RWMutext的使用還有幾個可能踩到的坑:

  • 不可復(fù)制,復(fù)制可能導(dǎo)致內(nèi)部計數(shù)值readCount、readWait的原子性遭到破壞,進而導(dǎo)致信號量的等待/釋放錯亂,最后導(dǎo)致鎖被重復(fù)釋放,或者鎖永遠被持有無法釋放等異?,F(xiàn)象。
  • 重入讀鎖導(dǎo)致死鎖(本文所示)
  • 釋放未加鎖的RWMutex,或重復(fù)釋放,本質(zhì)上是對RUnlock/Unlock操作的重入,RLock/Lock的重入會導(dǎo)致死鎖,重復(fù)解鎖會導(dǎo)致panic,所以RLock/RUnlock、Lock/Unlock一定要成對出現(xiàn)。

(2) pthread-c

讀寫鎖并不是golang的原創(chuàng),posix標(biāo)準(zhǔn)線程庫里也有對應(yīng)的實現(xiàn):

int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);

posix標(biāo)準(zhǔn)并未指定讀寫鎖的優(yōu)先級,但允許實現(xiàn)者采用writer-preferring來避免writer饑餓問題,

The pthread_rwlock_rdlock() function applies a read lock to the read-write lock referenced by rwlock. The calling thread acquires the read lock if a writer does not hold the lock and there are no writers blocked on the lock. It is unspecified whether the calling thread acquires the lock when a writer does not hold the lock and there are writers waiting for the lock. If a writer holds the lock, the calling thread will not acquire the read lock. If the read lock is not acquired, the calling thread blocks (that is, it does not return from the pthread_rwlock_rdlock() call) until it can acquire the lock. Results are undefined if the calling thread holds a write lock on rwlock at the time the call is made.

mplementations are allowed to favour writers over readers to avoid writer starvation

粗略追了一下glibc的源碼,pthread讀寫鎖的實現(xiàn)也是采用writer-preferring(__nr_writers_queued記錄正在等待寫鎖的writer)。

/* Acquire read lock for RWLOCK.  */
int
__pthread_rwlock_rdlock (rwlock)
     pthread_rwlock_t *rwlock;
{
  int result = 0;

  LIBC_PROBE (rdlock_entry, 1, rwlock);

  /* Make sure we are alone.  */
  lll_lock (rwlock->__data.__lock, rwlock->__data.__shared);

  while (1)
    {
      /* Get the rwlock if there is no writer...  */
      if (rwlock->__data.__writer == 0
   /* ...and if either no writer is waiting or we prefer readers.  */
   && (!rwlock->__data.__nr_writers_queued
       || PTHREAD_RWLOCK_PREFER_READER_P (rwlock)))
 {
   /* Increment the reader counter.  Avoid overflow.  */
   if (__builtin_expect (++rwlock->__data.__nr_readers == 0, 0))
     {
       /* Overflow on number of readers.  */
       --rwlock->__data.__nr_readers;
       result = EAGAIN;
     }

(3) C++

C++ 17提供了“共享鎖”這種語義的同步原語shared_mutex,類似于讀寫鎖,通過對shared_mutex加獨占鎖lock()和共享鎖lock_shared()來實現(xiàn)讀寫鎖語義,不過標(biāo)準(zhǔn)本身也沒有定義讀寫鎖優(yōu)先級。

Acquires shared ownership of the mutex. If another thread is holding the mutex in exclusive ownership, a call to lock_shared will block execution until shared ownership can be acquired.

If lock_shared is called by a thread that already owns the mutex in any mode (exclusive or shared), the behavior is undefined.

namespace std {
  class shared_mutex {
  public:
// ......
    // exclusive ownership
    void lock();                // blocking
    bool try_lock();
    void unlock();
    // shared ownership
    void lock_shared();         // blocking
    bool try_lock_shared();
    void unlock_shared();
    // ......
  };
}

查閱了一下資料,有網(wǎng)友做過一些優(yōu)先級相關(guān)的實驗,結(jié)果如下(實驗結(jié)果引用自引用地址):

  • gcc version 9.3.0的實現(xiàn)中,shared_mutex是讀優(yōu)先的
  • gcc version 10.2.0的實現(xiàn)中,shared_mutex是寫優(yōu)先的
責(zé)任編輯:趙寧寧 來源: 騰訊技術(shù)工程
相關(guān)推薦

2009-09-14 19:39:14

批量線程同步

2024-01-19 21:55:57

C++編程代碼

2012-05-18 11:17:58

Java多線程

2009-07-15 17:09:32

Swing線程

2022-09-06 08:02:40

死鎖順序鎖輪詢鎖

2010-04-20 11:56:30

Oracle物理結(jié)構(gòu)故

2011-09-27 09:42:01

Linux系統(tǒng)

2024-07-16 08:03:43

2024-09-26 00:00:10

死鎖阿里面試

2025-03-03 01:25:00

SpringAOP日志

2009-12-22 15:50:11

2010-06-07 09:22:21

MySQL+PHP亂碼

2018-01-11 21:32:45

機房漏電機房安全

2018-06-25 10:43:40

機房漏電方案

2009-03-18 09:26:23

Winform多線程C#

2009-09-10 14:00:00

2009-12-29 09:01:49

2014-09-10 09:58:39

U-Mail郵件系統(tǒng)

2021-08-31 07:57:21

輪詢鎖多線編程Java

2010-01-06 09:12:50

交換機安裝不當(dāng)故障
點贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 亚洲第一av | 欧美国产亚洲一区二区 | 成人亚洲在线 | 日韩一级免费看 | 亚洲国产精选 | 羞羞视频一区二区 | 在线啊v | 天天操夜夜操免费视频 | 国产精品久久久久久久久久妇女 | 97视频免费 | 精品国产一区二区三区性色av | 国产一级一级国产 | 欧美综合色 | 欧区一欧区二欧区三免费 | 嫩草最新网址 | 天天操天天干天天爽 | 久久国产成人精品国产成人亚洲 | 欧美三级三级三级爽爽爽 | 视频三区 | www.日本精品 | 亚洲va中文字幕 | 欧美日韩免费视频 | 自拍 亚洲 欧美 老师 丝袜 | 嫩草视频入口 | 999久久久久久久久6666 | 超碰97人人人人人蜜桃 | 欧产日产国产精品视频 | 最新超碰 | 日韩中文在线视频 | 一二区视频 | 日韩一级黄色片 | 亚洲黄色国产 | 久操av在线 | 久久久久一区二区三区四区 | 国产伦精品一区二区三区高清 | 欧美成人精品一区二区三区 | 日本一区二区高清不卡 | 精品日韩 | 日韩av在线一区 | 日本又色又爽又黄的大片 | 69av网 |