在整合FISCO BCOS非國密單測與國密單測的工做中,咱們發現CachedStorage的單測偶然會陷入卡死的狀態,且可在本地持續復現。 git
復現方式爲循環執行CachedStorage單測200次左右,便會發生一次全部線程均陷入等待狀態、單測沒法繼續執行的狀況,咱們懷疑在CachedStroage中發生了死鎖,故對此進行調試。github
中醫治病講究望聞問切,調試bug一樣須要遵循尋找線索、合理推斷、驗證解決的思路。多線程
在死鎖發生時,使用/usr/bin/sample工具(mac平臺環境下)將全部的線程的棧打印出來,觀察各線程的工做狀態。函數
從全部線程的線程棧中觀察到有一個線程(此處稱爲T1)卡在CachedStorage.cpp的第698行的touchCache函數中,具體的代碼實現能夠參考:工具
https://github.com/FISCO-BCOS...區塊鏈
從代碼片斷中能夠看到,T1在第691行已經得到了m_cachesMutex的讀鎖:代碼RWMutexScoped(some_rw_mutex, false)的意思是獲取某個讀寫鎖的讀鎖;相應地,代碼RWMutexScoped(some_rw_mutex, true)的意思是獲取某個讀寫鎖的寫鎖,這裏的RWMutex是一個Spin Lock。ui
隨後在第698行處嘗試獲取某個cache的寫鎖。spa
除了T1,還有另一個線程(此處稱爲T2)卡在CachedStorage.cpp的第691行的touchCache函數中:.net
從代碼片斷中能夠看到,T2在第681行已經得到了某個cache的寫鎖,隨後在第691行處嘗試獲取m_cachesMutex的讀鎖。線程
繼續觀察後還發現若干線程卡在CachedStorage.cpp第673行的touchCache函數中:
最後還有一個Cache清理線程(此處稱爲T3)卡在CachedStorage.cpp的第734行的removeCache函數中:
從代碼片斷中能夠看到,這些線程均沒有持任何鎖資源,只是在單純地嘗試獲取m_cachesMutex的寫鎖。
初期分析問題時,最詭譎的莫過於:在T1已經獲取到m_cachesMutex讀鎖的狀況下,其餘一樣試圖獲取m_cachesMutex讀鎖的線程居然沒法獲取到。
可是看到T3線程此時正努力嘗試獲取m_cachesMutex寫鎖,聯想到讀寫鎖飢餓問題,咱們認爲其餘線程獲取不到讀鎖的問題根源極可能就在T3。
所謂讀寫鎖飢餓問題是指,在多線程共用一個讀寫鎖的環境中,若是設定只要有讀線程獲取讀鎖,後續想獲取讀鎖的讀線程都能共享此讀鎖,則可能致使想獲取寫鎖的寫線程永遠沒法得到執行機會(由於讀寫鎖一直被其餘讀線程搶佔)。
爲了解決飢餓問題,部分讀寫鎖會在某些狀況下提升寫線程的優先級,即由寫線程先佔用寫鎖,而其餘讀線程只能在寫線程後乖乖排隊直到寫線程將讀寫鎖釋放出來。
在上述問題中, T1已經獲取了m_cachesMutex的讀鎖,若此時T3剛好得到時間片並執行到CachedStorage.cpp的第734行,會因獲取不到m_cachesMutex的寫鎖而卡住,隨後其餘線程也開始執行併到了獲取m_cachesMutex讀鎖的代碼行。
若讀寫防飢餓策略真的存在,那這些線程(包括T2)的確會在獲取讀鎖階段卡住,進而致使T2沒法釋放cache鎖,從而T1沒法獲取到cache鎖,此時全部線程均會陷入等待中。
在這個前提下,彷佛一切都能解釋得通。上述流程的時序圖以下所示:
咱們找到了TBB中Spin RW Lock的實現代碼,以下圖所示:
獲取寫鎖:
獲取讀鎖:
在獲取寫鎖的代碼中,能夠看到寫線程若是沒有獲取到寫鎖,會置一個WRITER_PENDING標誌位,代表此時正有寫線程在等待讀寫鎖的釋放,其餘線程請勿打擾。
而獲取的讀鎖代碼中,也能夠看到,若是讀線程發現鎖上被置了WRITER_PENDING標誌位,就會老實地循環等待,讓寫線程優先去獲取讀寫鎖。這裏讀寫鎖的行爲完美符合以前對讀寫鎖防飢餓策略的推測,至此真相大白。
既然找到了問題原由,那解決起來就容易多了。在CachedStorage的設計中,Cache清理線程優先級很低,調用頻率也不高(約1次/秒),所以給予它高讀寫鎖優先級是不合理的,故將removeCache函數獲取m_cachesMutex寫鎖方式作以下修改:
修改後,獲取寫鎖方式跟獲取讀鎖相似:每次獲取寫鎖時,先try_acquire,若是沒獲取到就放棄本輪時間片下次再嘗試,直到獲取到寫鎖爲止,此時寫線程不會再去置WRITER_PENDING標誌位,從而可以不影響其餘讀線程的正常執行。
相關代碼已提交至2.5版本中,該版本將很快與你們見面,敬請期待。
修改前循環執行CachedStorage單測200次左右便會發生死鎖;修改後循環執行2000+次仍未發生死鎖,且各個線程均能有條不紊地工做。
從此次調試過程當中,總結了一些經驗與你們分享。
首先,分析死鎖問題最有效的仍然是「兩步走」方法,即經過pstack、sample、gdb等工具看線程棧,推測致使發生死鎖的線程執行時序。
這裏的第二步須要多發揮一點想象力。以往的死鎖問題每每是兩個線程間交互所致使,教科書上也多以兩個線程來說解死鎖四要素,但在上述問題中,因爲讀寫鎖的特殊性質,須要三個線程按照特殊時序交互才能夠引起死鎖,算是較爲少見的狀況。
其次,『只要有線程獲取到讀鎖,那其餘想獲取讀鎖的線程必定也能獲取讀鎖』的思惟定勢是有問題的。
至少在上面的問題中,防飢餓策略的存在致使排在寫線程後的讀線程沒法獲取讀鎖。但本文的結論並不是放之四海而皆準,要不要防飢餓、怎麼防飢餓在各個多線程庫的實現中有着不一樣的取捨。有的文章提到過某些庫的實現就是遵循『讀線程絕對優先』規則,那這些庫就不會遇到這類問題,因此仍然須要具體問題具體分析。
《超話區塊鏈》話題徵集:個人Debug經歷
歡迎與咱們聊聊你經歷過的有趣/難忘的debug過程,咱們將挑選具有參考意義的debug經歷與更多開發者分享,經社區採納將可得到FISCO BCOS記念衫一件。
Debug經歷徵集傳送門:
FISCO BCOS的代碼徹底開源且免費下載地址↓↓↓
https://github.com/FISCO-BCOS...