序言算法
某日,開發哥哥一如往常的在線上發佈版本,kill掉應用程序後啓動新程序,程序啓動後,應用程序就一直阻塞在某處,因而版本回退,重啓舊版本,應用程序依舊阻塞在某處。pstack查看進程棧後發現,原來是第一次被kill掉的程序是運行在臨界區時被kill的,而代碼又有bug,在申請鎖的時,未對這種狀況「佔着資源的死去」進行處理,致使後續程序再申請鎖時只拋異常,而不釋放資源。sql
那這個bug測試應該怎麼模擬呢?通常程序經常使用哪些鎖呢?針對進程/線程鎖什麼場景下須要鎖呢?加鎖的影響是什麼呢?鎖的粒度應該如何控制呢?怎麼測試「鎖」呢?本文爲說明這類問題,分如下結構進行總結:安全
一:測試的「經典缺陷」 二:鎖的經常使用基礎知識 1、Linux多進程同步方式對比 2、常見的鎖類別 3、互斥鎖和Mysql鎖的使用場景 4、加鎖的影響和鎖的粒度 三:鎖的測試方法和策略
1、測試的「經典缺陷」多線程
缺陷定義 : 以上描述場景,其實是鎖的一個經典的使用場景。程序在獲取了臨界資源後異常退出,臨界資源一直處於加鎖狀態,其餘進程/線程申請鎖程序未正常處理就會致使阻塞,甚至死鎖等待。併發
測試模擬 : 測試的時候,必須清楚鎖的使用各個場景和異常場景,若是隻是經過無計劃的大數據壓測,重現該場景的可能性很低,由於該類異常必須在臨界區kill進程,所以測試必須使用GDB打斷點,運行到臨界區的斷點後,kill進程才能模擬。而後再次啓動程序,pstack查看進程棧是否阻塞。app
缺陷修復 : 這類缺陷主要產生的問題就是,後續進程在申請鎖時,若是出現了「鎖被已死進程」佔有後,應該怎麼讓系統回收鎖的問題。本質上這個問題就是進程間互斥鎖回收問題。函數
下面是致使產生bug的代碼片斷: 高併發
void ProcessMutex::lock() throw(CException){ if( pthread_mutex_lock(m_pMutex) != 0){ throw CException(ERR_LOCK_CREATE, "Failed to lock mutex!", __FILE__, __LINE__);}}
修復方法:性能
第一步:設置強健屬性爲 PTHREAD_MUTEX_ROBUST_NP。只要在互斥鎖初始化時調用pthread_mutexattr_setrobust_np設置支持回收機制。測試
ProcessMutex::ProcessMutex(const char* path, int id) throw(CException): m_ShMem(path, id, sizeof(pthread_mutex_t), true)
{
m_pMutex = (pthread_mutex_t *)m_ShMem.address();
// 設置互斥量進程間可共享
if(m_ShMem.isCreator())
{
pthread_mutexattr_t mutex_attr;
pthread_mutexattr_init(&mutex_attr);
pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED);
pthread_mutexattr_setrobust_np(&mutex_attr, PTHREAD_MUTEX_ROBUST_NP);
pthread_mutex_init(m_pMutex, &mutex_attr);
pthread_mutexattr_destroy(&mutex_attr);
}
}
第二步:捕獲獲取鎖返回異常EOWNERDEAD,調用pthread_mutex_consistent_np完成鎖owner的切換工做便可。
void ProcessMutex::lock() throw(CException) { int iErrno = pthread_mutex_lock(m_pMutex); if( iErrno != 0) { if ( iErrno == EOWNERDEAD ) { pthread_mutex_consistent_np(m_pMutex); } else { throw CException(ERR_LOCK_CREATE, "Failed to lock mutex!", __FILE__, __LINE__); } } }
二 :鎖的經常使用基礎知識
下面的介紹,主要是基於測試過程當中遇到的各類鎖的一些概括小結,有些其餘更高級的鎖暫未接觸到,後面有接觸到再更新本文章吧~~~
一、Linux多進程同步方式對比
在進行多進程開發的時候,常常會遇到各類進程間同步的場景,Linux多進程同步機制的性能和功能均有較大差別 ,通常使用如下4種方式:
從功能上分析:原子操做< mutex < 信號量 < 記錄鎖。原子操做只支持有限的幾種整數運算;mutex只支持加鎖和解鎖兩種狀態;信號量則支持計數;記錄鎖功能最爲豐富,能支持讀寫鎖、區間鎖、屢次加鎖一次釋放、進程退出自動釋放等功能。
那性能呢?簡單的測試方法:程序分別啓動1~5個子進程,在共享內存中存放一個int整數,每一個子進程對其自增1M次,總計時間,程序運行5次取均值。(時間單位爲毫秒),結果性能排名是:原子操做 > mutex > 信號量 > 記錄鎖。記錄鎖甚至在單進程的狀況下性能都低於mutex在5個進程下的表現,到多進程的時候性能比其它同步操做低了一個數量級以上。結果以下:
二、常見的鎖類別
第一類:unix內核級別鎖。這類鎖常用,針對於多進程或者多線程的程序在運行的過程當中,有時會出現公共資源搶佔使用的狀況就會使用到這類鎖,這類鎖經常使用的分如下4類:
互斥鎖其實是一種變量,在使用互斥鎖時,其實是對這個變量進行置0置1操做並進行判斷使得線程可以得到鎖或釋放鎖。 提供兩種得到鎖方法,經常使用的是pthread_mutex_lock:
pthread_mutex_lock:若是此時已經有另外一個線程已經得到了鎖,那麼當前線程調用該函數後就會被掛起等待,直到有另外一個線程釋放了鎖,該線程會被喚醒。
pthread_mutex_trylock:若是此時有另外一個賢臣已經得到了鎖,那麼當前線程調用該函數後會當即返回並返回設置出錯碼爲EBUSY,即它不會使當前線程掛起等待
而互斥鎖的底層實現,通常使用swap或exchange指令,這個指令的含義是將寄存器和內存單元中的數據進行交換,這條指令保證了操做lock和unlock的原子性。
第二類:文件鎖:FileLock;防止多進程併發;是一種文件讀寫機制,在任何特定的時間只容許一個進程訪問一個文件。
第三類:Mysql鎖。根據鎖類型:共享鎖(讀鎖),排他鎖(寫鎖);根據鎖策略:表鎖,行鎖,間隙鎖 ;根據鎖方法:悲觀鎖,樂觀鎖
三、互斥鎖和Mysql鎖的使用場景
針對互斥鎖,主要在如下三個常見場景常用:
DB句柄:主讀,子讀 DB句柄在主線程/全局變量內定義,子線程須要更句柄來更新數據
非線程安全的API使用: SHA256簽名,Rsa256 Localtime ->localtime_r
針對Mysql鎖,主要分事務內和事務外:
四、加鎖的影響和鎖的粒度
錯誤的使用:
unsigned char* digest = SHA256((unsigned char *)strUnSign.c_str(), strUnSign.size(), NULL);
正確的使用,這樣後面使用該算法時,openssl庫的鎖能保證併發的可靠性
unsigned char digest[SHA256_DIGEST_LENGTH] = {0}; SHA256((unsigned char *)strUnSign.c_str(), strUnSign.size(), digest);
三:鎖的測試方法和策略
測試方法:
其中第一個和第二個方法主要是針對已知加鎖的地方進行測試;第三個和第四個方法用於肯定是否須要鎖
測試點: