作者:vollin
2009年4月
目錄
引言
一、 線程安全相關(guān)的概念
(一) 可重入性
(二) 線程安全
(三) 異步信號安全
(四) 三者的異同
二、 可重入準(zhǔn)則
(一) 純代碼(purecode)函數(shù)一定是可重入函數(shù)
(二) 使用了局部靜態(tài)變量的函數(shù),是不可重入的
(三) 調(diào)用了不可重入函數(shù)的函數(shù),是不可重入的
(四) 使用了同步機(jī)制的函數(shù),是不可重入的
三、 Easy線程安全
(一) 弱化信號處理函數(shù):
(二) 編寫線程安全的類
(三) 簡化數(shù)據(jù)同步
(四) 應(yīng)用實例
結(jié)束語
參考資料:
附錄1:基于互斥鎖的安全對象及輔助類的完全實現(xiàn)
附錄2:使用附錄1代碼的一個簡單示例
并發(fā)/并行編程中多線程編譯占有重要的地位,編寫一份線程安全的程序即使對于一名經(jīng)驗豐富的開發(fā)人員也是一種挑戰(zhàn)。本文將通過詳解線程安全的上的相關(guān)概念,確定出一系列的原則,幫忙多線程程序的開發(fā)人員能夠容易的檢查判斷自己所編譯的程序是否是線程安全的。相信這部分內(nèi)容對所有有計劃或已經(jīng)編寫過多線程程序的程序員會有一定的幫助。
同時根據(jù)部分易于產(chǎn)生線程安全問題的情況,針對C++編寫多線程程序給出了一些實用的技術(shù)及相關(guān)代碼供大家參考。這部分內(nèi)容的閱讀需要讀者對C++有一定的了解;當(dāng)然就其給出的思想也部分適用于其它面象對象的編程語言。
可重入性(reentrant)是針對函數(shù)的,它有兩個方面的內(nèi)涵:
1, 可并行/并發(fā)
[vollin1] 同時進(jìn)入:指可重入函數(shù)被某任務(wù)調(diào)用時,其它任務(wù)可同時進(jìn)行調(diào)用而不產(chǎn)生錯誤的結(jié)果;或稱在相同的輸入情況下可重入函數(shù)的執(zhí)行所產(chǎn)生的效果,并不因其并發(fā)的調(diào)用而產(chǎn)生不同,也稱并發(fā)安全。
2, 中斷后可重新進(jìn)入:指可重入函數(shù)可被任意的中斷,當(dāng)中斷執(zhí)行完畢,返回斷點可以繼續(xù)正確的運行下去;或稱在相同的輸入情況下可重入函數(shù)的執(zhí)行所產(chǎn)生的結(jié)果,并不因為在函數(shù)執(zhí)行期間有中斷的調(diào)用而產(chǎn)生不同,也稱中斷安全。
線程安全(MT-safe)不僅僅針對函數(shù),它主要是指數(shù)據(jù)或程序的安全這一結(jié)果,所以有不同的層次上講的線程安全:如線程安全的函數(shù),線程安全的庫
[vollin2] 。本文還會引入線程安全的類這一概念。通常意義上一個線程安全的函數(shù)是指有可重入函數(shù)第一個內(nèi)涵的函數(shù)即并發(fā)安全的函數(shù)。但需要注意的是即使是一個從函數(shù)級別上并不安全的函數(shù),如果使其不安全的因素在特定應(yīng)用中并不存在時,這個函數(shù)對于該應(yīng)用來講同樣也是線程安全的。例如對于全局變量的訪問,一般而言未命名同步方式訪問肯定是非線程安全的,但如果所有可能同時發(fā)生的訪問均是只讀訪問則從結(jié)果上講也是線程安全的。
信號的本質(zhì)軟中斷,中斷在本質(zhì)上是異步的,所謂異步信號安全同線程安全一樣,也是占在結(jié)果上考慮的,指在信號的中斷處理過程中的安全。通常意義上一個異步信號安全的函數(shù)是指可以在異步信息處理函數(shù)中調(diào)用而不會出現(xiàn)異常的函數(shù)。同樣需要注意到即使一個從函數(shù)級別上并非異步信息安全的函數(shù),如果在信息處理函數(shù)中調(diào)用,也并不一定會產(chǎn)生不安全的結(jié)果。
對于一個多線程程序的安全來講,通常包括了線程安全和異步信號安全這兩個部分。從函數(shù)級別考慮,僅從概念上就可以發(fā)現(xiàn)可重入函數(shù)一定是線程安全函數(shù),也是異步信號安全函數(shù);多線程安全函數(shù)卻要弱得多,并非一定要是可重入函數(shù),它只要求并發(fā)無誤即可;雖然異步信號函數(shù)與可重入函數(shù)的描述方式有所不同,但兩者從實現(xiàn)層面上講是完全一致的,或稱可重入函數(shù)與異步信號安全函數(shù)這兩個概念是等價的。
如何編寫或判斷一個函數(shù)是否是可重入的呢?以下具體例出的一些經(jīng)驗將幫助您的編寫和判斷。
(purecode)函數(shù)一定是可重入函數(shù)
如果你編寫的一個函數(shù)中所使用的所有數(shù)據(jù)均是局部變量(可以為寄存器變量,因為中斷時寄存器中的數(shù)據(jù)也會入棧的)或形參,沒有使用任何外部的全局變量和內(nèi)部的靜態(tài)變量,也沒有使用系統(tǒng)函數(shù),這樣的函數(shù)就可以稱為純代碼。
例:
int Addition(int n)
{
int s=0;
for(register i=1;i<=n;i++)
{
s=s+i;
}
return s;
}
特別提示:一般僅僅只有僅用于計算的函數(shù)才屬于此類型。
使用局部靜態(tài)變量的函數(shù)都是不可重入的,但可以通過改變參數(shù)的方式進(jìn)行簡單的調(diào)整使其可重入,以下是一個示例:
//不可重入的版本
char* Str2Lower(char *s)
{
static char buffer[STR_MAX_LEN]={0};
for(int i=0;’/0’ != str[i];i++)
{
buffer[i]=tolower(s[i]);
}
buffer[i]=’/0’;
return buffer;
}
//可重入的版本
char* Str2Lower_r(char *sIn, char* sOut)
{
for(int i=0;’/0’ != sIn[i];i++)
{
sOut[i]=tolower(s[i]);
}
sOut[i]=’/0’;
return sOut;
}
特別提示:在函數(shù)內(nèi)使用靜態(tài)變量,但并不返回靜態(tài)變量相關(guān)的引用,指針等的函數(shù)可以通過對靜態(tài)變量的加解鎖機(jī)制使其線程安全;但如果有將靜態(tài)變量的相關(guān)信息返回就一定時非線程安全的。
調(diào)用了自己編寫的函數(shù)自不必待言,本節(jié)主要說明如何判斷一個系統(tǒng)函數(shù)是否一個可重入的函數(shù),以使得容易判斷自己編寫的調(diào)用了系統(tǒng)函數(shù)的函數(shù)是否可重入:
1, 系統(tǒng)函數(shù)的函數(shù)名中帶有“_r”后綴的函數(shù)都是可重入的,其對應(yīng)的無后綴的函數(shù)是不可重入的。這類函數(shù)多是字符串相關(guān),時間相關(guān)的函數(shù),路徑地址相關(guān)函數(shù);其它以非入?yún)⒅羔槥榉祷刂档暮瘮?shù)也應(yīng)當(dāng)重點懷疑可跳到第4點進(jìn)行判斷;
特別提示:這類非“_r”函數(shù)均在函數(shù)體中引用了全局變量或靜態(tài)變量,所以也都是非線程安全的函數(shù)。
2, 內(nèi)存分配與釋放函數(shù)是不可重入的,因為內(nèi)存的分配釋放函數(shù)維護(hù)了一個鏈表,這使得在正常處理函數(shù)中調(diào)用分配函數(shù)時,被中斷,且在中斷中再調(diào)用內(nèi)存分配釋放函數(shù)則會出現(xiàn)異常;
特別提示:這類函數(shù)是線程安全的。
3, IO相關(guān)函數(shù)是不可重入的,如printf,及其它與文件描述符相關(guān)的操作函數(shù)(文件IO,網(wǎng)絡(luò)IO,管道等本地同步IO);
特別提示:不涉及文件句柄的IO函數(shù)是線程安全的,涉及文件描述符的函數(shù)如果不是在對同一句柄操作則是線程安全的,僅當(dāng)在需要多線程對同一文件描述符做操作時才需要添加同步機(jī)制,當(dāng)然更好的辦法應(yīng)該是避免多線程有同時操作同一文件描述符的操作;特別的pread與pwrite即使在對同一個文件描述符操作時也是線程安全。
4, 最后一招:查系統(tǒng)man 手冊,或IEEE Std 1003.1,例:關(guān)于usleep,IEEE Std 1003.1中有如下描述
The usleep() function need not be reentrant. A function that is not required to be reentrant is not required to be thread-safe.
這表明這個函數(shù)即非可重入的,也非線程安全的。
同步機(jī)制對于可重入是沒有幫助的,或者說在中斷面前同步機(jī)制是無效的,所以有同步機(jī)制的函數(shù)肯定是不可重入的,也當(dāng)然不能夠在信號處理函數(shù)中調(diào)用,但這卻是最常用也最有效的消除線程不安全因素的手段。
特別提示:使用自旋鎖進(jìn)行同步是一種比較特殊的情況,自旋鎖的實現(xiàn)在各個平臺上都有所不同,而且也需要做特殊的處理才能夠用于信號處理函數(shù)中,所以從嚴(yán)格意義上講仍然不能算是可重入的函數(shù)。
線程安全
前文提到做為一個多線程程序,它的安全性需要從異步信號安全和線程安全兩方面考慮,下面提供了一些建議及一些技巧,以使得保證多線程安全更加輕松如意。
信號處理函數(shù)對于多進(jìn)程(特指每進(jìn)程僅有一個線程的多進(jìn)程)編程而言,是一個非常強(qiáng)有力的工具,因為對具體的每個進(jìn)程而言不具有并發(fā)性所以需要有一些異步的手段來解決實際的編程問題,并且由于僅有一個線程,所以可以控制信號在希望出現(xiàn)的地方出現(xiàn),這樣能夠避開在信號處理函數(shù)中僅能調(diào)用可重入函數(shù)的限制。但對于多線程編程而言信號處理的必要性就大大減弱,完全可以被多線程自身的并發(fā)性解決;另一方面由于多線程的并發(fā)造成無法保證所有的線程都在同一個段代碼中等待信號的出現(xiàn),這是與多線程編程的思想是相餑的,這也造成了如果要編寫安全的信號處理函數(shù),所受到的限制非常多,難于寫出強(qiáng)大的信號處理函數(shù)。
一種簡單易用,且絕對“安全”的信號處理方案如下:
volatile int g_nSignal=0;//用于保存最近一次信號值
void InitSignal(void)//初始化信號處理函數(shù)
{
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD,SIG_IGN);
signal(SIGTERM,RecvSignal);
signal(SIGHUP,RecvSignal);
signal(SIGALRM,RecvSignal);
signal(SIGXFSZ,RecvSignal);
}
void RecvSignal(int s)//通用信號處理函數(shù)
{
g_nSignal = s;
InitSignal();
}
bool HandleSignal()//真實的信號處理函數(shù),返回false表示退出
{
int nSignal = g_nSignal;
g_nSignal = 0;
Switch(nSignal)
{
case 0:
return true;
case ….
{
…..//相關(guān)處理
break;
}
case SIGTERM:
{
…//安全退出相關(guān)代碼
return false;
}
}
return true;
}
//建立專門的信號處理線程在其中執(zhí)行
while (HandleSignal()){…//休眠短暫的時間或定時處理其它事務(wù)};
_exit();
在這個信號處理方案中,信號處理函數(shù)被弱化到僅僅做為信號收集的程度,將通過異步處理信號的方式來避免信號處理函數(shù)中可能帶來的不安全因素。通過代碼閱讀大家可以發(fā)現(xiàn)前文在安全上加引號的原因:該方案在兩個或以上信號在很短時間之內(nèi)同時發(fā)生時,有可能產(chǎn)生覆蓋,但是可以絕對的保證程序的安全,如果程序的主要處理不依賴信號,僅用信號來處理一些關(guān)鍵性的非頻繁事件(如配置重載,程序退出等),則此方案可以絕對安全有效的完成。當(dāng)然這也是基于前面的假設(shè),我們要盡可能的弱化信號在程序處理中的做用。
實際上可以對以上方案做出少量的修改使得更加安全,但在信號處理的弱化這一大前提下,這樣的改進(jìn)也并非必要。
如果一定要編寫信號處理函數(shù),那么需要特別注意在這里只能使用可重入的函數(shù),否則極易出現(xiàn)段異常,慎之慎之。
前文對函數(shù)的線程安全做了較多的討論,如果對于純函數(shù)式的語言如C來講基本足夠了,但對于面向?qū)ο蟮恼Z言如C++,我們要使得程序安全,那么編寫線程安全的類就變得重要了。
線程安全的類是怎樣的一個概念呢?
1, 從STL的線程安全開始
并沒有相關(guān)的文章專門討論過類的線程安全,但似乎我們又經(jīng)常在用,比如我們經(jīng)常聽到說stl容器是線程安全的,但又有文章稱真正線程安全的stl容器并不存在,它們的安全需要程序員自己保證。怎么回事呢?我們要可以查到關(guān)于stl容器線程安全相關(guān)的聲明:
n 多個讀取者是安全的。多線程可能同時讀取一個容器的內(nèi)容,這將正確地執(zhí)行。當(dāng)然,在讀取時不能有任何寫入者操作這個容器。
n 對不同容器的多個寫入者是安全的。多線程可以同時寫不同的容器。
這樣的聲明清晰而不足夠,本文引入線程安全的類這樣的概念不但將有利用大家理解上面關(guān)于stl線程安全與不安全的爭論,而且將利于大家編寫出線程安全的類以及在此基礎(chǔ)之上的線程安全的程序。
2, 線程安全類的定義
一個線程安全的類是指滿足以下條件的類:
靜態(tài)成員函數(shù)是線程安全的,即在多個線程調(diào)用該類的同一靜態(tài)成員函數(shù),能夠正確的執(zhí)行;
對于同一實例的const成員函數(shù)調(diào)用,可以同時安全的進(jìn)行,當(dāng)然在此過程中該實例不能有非const成員函數(shù)的調(diào)用或成員變量的寫入操作;
非靜態(tài)成員函數(shù)對于不同的實例是線程安全的,即可以在多個線程中以不同的實例調(diào)用同一動態(tài)成員函數(shù),能夠正確的執(zhí)行。
只要一個類滿足以上的三個條件,我們就可以在不同的線程中同時安全的使用該類的不同實例;如果需要多線程訪問該類的同一實例時也只需要同使用一般全局?jǐn)?shù)據(jù)變量一樣,即:如果只有同時的讀取則可以直接使用,如果有可能有寫入操作那么就應(yīng)該使用同步的方式來保證其數(shù)據(jù)的安全。
怎樣理解這三個條件呢?類從設(shè)計的角度來講就是一組相關(guān)的數(shù)據(jù)及其上的操作的集合,即類同時具有數(shù)據(jù)及函數(shù)兩種特質(zhì),所以要使其安全也須要考慮這兩個方面的因素。
a) 類的靜態(tài)成員函數(shù)在使用方式上實際上同全局函數(shù)相差無幾,所以我們對它的要求就如果一般的全局函數(shù)一樣;所以一個線程安全的需要有線程安全的靜態(tài)函數(shù);
b) 類的具體實例數(shù)據(jù)(非靜態(tài)數(shù)據(jù))的訪問上實際與一般的數(shù)據(jù)變量的訪問相差無幾,所以要求也一樣:在只有只讀訪問時是完全的,在有寫操作參與時所有操作均需要同步;而const成員函數(shù)在使用上是只當(dāng)作對實例的只讀訪問的,所以需要這樣的保證,實際上只需要多注意一下mutable這種數(shù)據(jù)成員變量,做出控制就可以達(dá)到這一點;
c) 類的非靜態(tài)成員函數(shù)不僅可能涉及到具體實例的數(shù)據(jù)訪問(這應(yīng)當(dāng)由類的使用者去保證安全),而且它又同時是一個函數(shù),要線程安全當(dāng)然需要保證在對全局變量或靜態(tài)變量的使用符合線程安全的規(guī)范,這樣才能夠保證不同實例在多線程實用同一非靜態(tài)函數(shù)時不會受到相互的影響。
d) 這三類成同函數(shù)是有交集的,不過很明顯三者的要求,是依次減弱的,即對于靜態(tài)const成員函數(shù)需要滿足靜態(tài)成員函數(shù)的要求;非靜態(tài)const成員函數(shù)需要滿足const成員函數(shù)的要求。
3, 到STL結(jié)束
回來STL的線程安全與不安全的爭論,我們可以這樣理解了:STL容器類是線程安全的類,但對于同一實例的多線程訪問,還是需要由使用者去保證安全的。這就如一個其它任何共享數(shù)據(jù)要在多線程間共享一樣,不外如是。
當(dāng)然也應(yīng)該注意到一個線程安全的類,與一個安全的結(jié)果仍然是不同的。很多時候我們自己定義的類,其實并不滿足線程安全的條件,但只要注意其使用方式也是可以保證線程安全的。但如果是編寫一個提供給其它程序員使用的類,則一定要注意線程安全類的規(guī)則,如果有不符的地方一定要做出特別的說明避免使用者進(jìn)入陷阱,否則可是會遭人腹緋的哦。
多線程編程的一個極大的好處是多個線程共享存儲空間,使得共享數(shù)據(jù)較多進(jìn)程方便了不知幾許倍。但數(shù)據(jù)的共享也帶來了同步的問題,這無疑是多線程安全中最為重要的一環(huán),也是多線程編程的重點之一,更是易于出現(xiàn)錯誤的地方,所以需要通過提高其抽象層次降低其出現(xiàn)錯誤的可能。
多線程同步的機(jī)制/方式多種多樣,事件,信號量,條件變量,原子操作,鎖,關(guān)鍵代碼段等;僅拿鎖來講,就有各種分類的方式,各種鎖如互拆鎖,讀寫鎖,自旋鎖,遞歸鎖等,以及他們的組合如遞歸自旋鎖,自旋讀寫鎖等;這些具體方式的討論主要與同步效率有關(guān),本文主要討論線程安全所以對同步的具體方式和鎖的類型不作多的討論,僅以POSIX下最常用的默認(rèn)線程互斥鎖為例給出一些技巧以使得數(shù)據(jù)同步更加方便且不易出現(xiàn)錯誤。
1, 共享數(shù)組的組織
面向?qū)ο缶幊痰囊粋€極大的好處是可以提高編程設(shè)計的層次,這主要體現(xiàn)在能夠?qū)⒁恍┫嚓P(guān)的數(shù)據(jù)及操作組織在一起形成一個單獨的邏輯體――類。毫無疑問我們應(yīng)當(dāng)將總是在一起訪問的共享數(shù)據(jù)組織在一起。另外我們發(fā)現(xiàn)每組共享數(shù)據(jù)的使用總是與一個鎖同時使用
[vollin3] 。根據(jù)類的設(shè)計原則,這個鎖對象無疑也應(yīng)當(dāng)與數(shù)據(jù)組織在一起。這將使得共享數(shù)據(jù)的訪問更加方便和容易,然而如果我們只有一項數(shù)據(jù)要為這項數(shù)據(jù)專門設(shè)計一個類,是否反而會帶來麻煩?
其實自從有了模板類以后,這樣的煩惱是有解決辦法的,以下的代碼給出了一個解決方案。
template<class _Tp,class _Mutex = JXMutex>
class JSafe
{
public:
typedef _Tp value_type;
typedef _Mutex lock_type;
JSafe(void);
~JSafe(void);
explicit JSafe(const _Tp& other);
JSafe(const JSafe& other);
JSafe& operator =(const JSafe& other);
void Lock() const;
void Unlock() const;
void Value(const _Tp& other);//設(shè)置值
const _Tp Value(void) const;
_Tp& Data(void); //加鎖后方可安全使用此函數(shù)得到資源的引用,如未加鎖直接調(diào)用可能造成相應(yīng)的安全問題
protected:
volatile _Tp m_Data; //數(shù)據(jù)成員
mutable _Mutex m_Mutex; //互斥鎖成員
};
具體的實現(xiàn)很簡單,因為涉及后文講的技巧,此處沒有列出,具體的實現(xiàn)可參見附錄給出的代碼。其中關(guān)于鎖成員采用的是mutable 類型,這使得非數(shù)據(jù)的修改都可以是const的,在使用上更像是對常量的訪問。JXMutex類是一個簡單的互斥鎖類。
可以看到有了這樣的一個模板類,就可以很方便的將一個數(shù)據(jù)類型與一個互斥鎖組織在了一起,從該模板類提供了一些簡單安全設(shè)置及獲取數(shù)據(jù)的操作。有點需要注意的是_Tp& Data(void)這個函數(shù),及取出的數(shù)據(jù)引用都需要在鎖的保護(hù)下操作;根據(jù)這樣的思路,我們也可以針對具體的一些常用共享數(shù)據(jù)類型做一些擴(kuò)展,甚至可以使得它們可以像一般的數(shù)據(jù)一樣簡單的使用,如下面這個安全數(shù)字類:
template<class _Tp>
class JSafeNum : public JSafe<_Tp>
{
public:
typedef JSafe<_Tp> safe_type;
explicit JSafeNum(const _Tp& _Value = 0);
JSafeNum& operator ++();
JSafeNum operator ++(int);
JSafeNum& operator +=(const _Tp& other);
JSafeNum& operator --(void);
JSafeNum operator --(int);
JSafeNum& operator -=(const _Tp& other);
};
typedef JSafeNum<int> JSafeInt;
typedef JSafeNum<u_int> JSafeu_int;
。。。
有了這樣的一個安全數(shù)字類,關(guān)于數(shù)字的相關(guān)處理全都封裝成線程安全的方法,要寫一個全局計算器,只需要:
JSafeInt g_Count;//定義全局安全共享計算器
++g_Count;//在每個線程中如是計數(shù)
同樣我們還可以定義出使作stl的安全序列容器,當(dāng)然只能包括一些常用的操作,如常對共享的stl序列容器做的操作push_back,swap等,所有的方法都包裝是不太現(xiàn)實的。但對于我們向共享序列中加入新的項,或全面處理時都會更加簡單高效,而不易產(chǎn)生錯誤。
將鎖與數(shù)據(jù)組成為一個新的類這種方式與為每組共享數(shù)據(jù)定義一個全局的鎖這種方式來講,不但少了為各種全局鎖取名的麻煩,鎖與數(shù)據(jù)對應(yīng)的麻煩,減少了出錯的可能;更令程序的數(shù)據(jù)組織結(jié)構(gòu)顯得清晰易讀,程序的易讀性在很多時候甚至比效率更加的重要。
2, 異常安全的鎖使用
鎖的加解鎖需要嚴(yán)格配對,否則將造成死鎖。也許這并不是一個很嚴(yán)格的要求,似乎程序員只要仔細(xì)一些就可以滿足這個要求;然而實際并不盡如此。試想以下幾種場景:
a) 在鎖保護(hù)的代碼段中便需要return:難道只能使用C++開發(fā)人員難以接受的goto語句?又或者組織一個夸張的多層次嵌套的條件從句?這樣當(dāng)然也能夠完成任務(wù),但出現(xiàn)疏漏的可能性大量增加,代碼的閱讀也將令人窒息;
b) 在鎖保護(hù)的代碼段中調(diào)用了一些可能產(chǎn)生異常的函數(shù):難道每次加鎖后都需要加一個try,catch語句?然而這些異常并不應(yīng)該在本函數(shù)層次中進(jìn)行處理啊,這將令寫代碼與閱讀代碼都成為一種折磨。當(dāng)然更多的情況是沒有try,異常還是產(chǎn)生了,并由上一級代碼處理,而死鎖也就這樣產(chǎn)生了。
怎樣才能寫出異常安全的鎖的使用?怎樣既保證加的鎖得到釋放而又不用花費太多的心力?很自然的,我們想起了類的構(gòu)造和析構(gòu),如果不是使用new,他們就是嚴(yán)格配對的,并且對于析構(gòu)的控制并不需要手工的進(jìn)行,在跨出了定義對象的大括號時
[vollin4] ,就自動的析構(gòu)了。下面給出了一個輔助加鎖類的實現(xiàn)。
template<class _Tp>
class JLockHelper:
{
public:
JLockHelper(_Tp& xMutex):m_xMutex(xMutex)
{
m_xMutex.Lock();
}
~JLockHelper()
{
m_xMutex.Unlock();
}
protected:
_Tp& m_xMutex;
};
#define LOCK(mclass,mvalue) JLockHelper<mclass > __lock__(mvalue)
這個實現(xiàn)如果我們需要對一個包含Lock(),Unlock()包成員函數(shù)的鎖類或JSafe類實現(xiàn)加解鎖,只需要在需要在一個大括號范圍內(nèi)定義一個JLockHelper對象即可,當(dāng)大括號退出則自動解鎖,如果某段代碼中可能用到多個鎖,也最好使用一組嵌套的大括號,將每個鎖的控制范圍括起來,這將使得每個鎖的控制范圍都更加的清晰易讀。
如果使用Lock宏,則更加簡單,因為每個輔助加鎖變量的名字都相同,所以如果不用嵌套的大括號分開,還會出現(xiàn)編譯器的錯誤提示,提示程序員需要加上大括號以標(biāo)記不同鎖的作用范圍。
我們可以在任意的位置return出當(dāng)前函數(shù),當(dāng)前函數(shù)中產(chǎn)生任何的異常也不用擔(dān)心,輔助加鎖類的析構(gòu)函數(shù)會自動幫且我們解除鎖定。
具體的使用方式如下:
JSafe<myclass> g_safe; //全局共享的安全myclass類
JSafe<otherclass> g_othersafe; //全局共享的安全otherclasss類
某線程使用的一個函數(shù)中訪問時:
Void DoSomethgin()
{
….
{//對g_safe的同步訪問
LOCK(JSafe<myclass>,g_safe);
if(…)
{
…….
return;
}
else
{//對g_othersafe的同步訪問
LOCK(JSafe<otherclass>,g_othersafe);
…….//一些可能引起異常的調(diào)用
}
……
}
….
}
這樣的使用方式是否令你感覺到對于多線程的同步來講,真的是非常簡單且決計不易出錯。附錄1還給出了另一組幾乎類似的實現(xiàn)方案,可以使得輔助對象的使用Easy到令你心動以至于想立即試用一下。
3, 其它的同步技巧
關(guān)于此點,其實已經(jīng)超出來線程安全的討論范圍,不過與前兩點的使用有關(guān),所以隨便提及:
a) 關(guān)于JSafe類的成員函數(shù)Data(),必須在鎖控制下操作,并且其取出的數(shù)據(jù)也必須在鎖控制下操作,這實際上似乎是一種退化,這個函數(shù)也不過是為了該類的完整才提供出來??梢哉f在絕大多數(shù)的情況下不應(yīng)該使用這個函數(shù)。首先對于一個簡單的數(shù)據(jù)對象,如原生數(shù)據(jù)對象,這個函數(shù)是沒有使用必要的可以用Value()函數(shù)來取代;其次對于一個復(fù)雜的數(shù)據(jù)對象,可以從JSafe中繼承出來,為其特殊操作,做一個安全的包裝,以使得其操作能夠更加安全的進(jìn)行;
b) stl容器對象,在多線程的數(shù)據(jù)交互中有很重要的地位,如何安全并且高效的使用?這里需要特別注意一個叫做swap的函數(shù),特別對于非連續(xù)存儲的list,map之類它的代價非常之低,以至于你可以在需要處理時先用一個空對象swap出來,有必要再swap或添加到同步數(shù)據(jù)中去,這不但能夠避免安全風(fēng)險,更能夠減少鎖定的時間,提高程序效率。
對于前面提到的一些技巧,給出了一個具體的實例,包括安全對象及輔助加鎖類的實現(xiàn)(見附錄1),以及一個類似于文件過濾器的多線程小程序(見附錄2)。
大家可以在附錄中找到具體的代碼,代碼可以在linux下g++ *.cpp –lpthread直接編譯通過。大家可以從實例中看到,對于安全對象的使用會帶來多大的便利,與使用原始的同步方式相比,不僅對于代碼可閱讀性產(chǎn)生無可比擬的提升,更使得多線程的編寫成為了一種樂趣。
雖然本文提供了一些使得我們編寫安全的多線程程序更加容易的方法和技巧,但是我仍然還是要說使一個多線程程序安全是一門藝術(shù),也是一個挑戰(zhàn)。它所涉及的內(nèi)容遠(yuǎn)遠(yuǎn)超過本文所述,但它也并非那么遙不可及,它是我們觸手可及的,只要我們多用心,多總結(jié)。
1.《使用可重入函數(shù)進(jìn)行更安全的信號處理》
http://www.ibm.com/developerworks/cn/linux/l-reent.html2.《IEEE Std 1003.1》 參見1中的參考資料
3.《多線程編程指南》
http://docs.sun.com/app/docs/doc/819-7051/mtintro-75924/*
* =====================================================================================
*
* Filename: safe.h
*
* Description: MT safe
*
* Version: 1.0
* Created: 04/30/2009 01:00:58 PM CST
* Revision: none
* Compiler: gcc
*
* Author: vollinwan,
* Company:
*
* =====================================================================================
*/
#include <sys/types.h>
#include <pthread.h>
#ifndef __VOLLIN_J_SAFE_H__
#define __VOLLIN_J_SAFE_H__
#define LOCK(Mutex) JLockHelper __lock__(Mutex)
class JLockHelper;
struct JAbsXMutex
{
virtual void Lock() const= 0;
virtual void Unlock() const= 0;
};
/*****************************************************
* JXMutex X鎖(互斥鎖)類
*****************************************************/
class JXMutex:public JAbsXMutex
{
public:
JXMutex()
{
pthread_mutex_init (&m_Mutex,NULL);
}
virtual ~JXMutex()
{
pthread_mutex_destroy(&m_Mutex);
}
private:
JXMutex(const JXMutex&); //禁止鎖拷貝
const JXMutex& operator =(const JXMutex &); //禁止鎖賦值
public:
void Lock() const
{
pthread_mutex_lock (&m_Mutex); //鎖定
}
void Unlock() const
{
pthread_mutex_unlock (&m_Mutex); //解鎖
}
public:
mutable pthread_mutex_t m_Mutex; //互斥鎖
};
/**************************************************************************************
* JSafe 基于各類鎖的安全類模板
**************************************************************************************/
template<class _Tp,class _Mutex = JXMutex>
class JSafe
{
public:
typedef _Tp value_type;
typedef _Mutex lock_type;
JSafe(void){}
~JSafe(void){}
explicit JSafe(const _Tp& other)
{
m_Data = other;
}
JSafe(const JSafe& other)
{
LOCK(other);
m_Data = other.m_Data;
}
operator const _Tp() const //定義強(qiáng)制類型轉(zhuǎn)換,注意已鎖定時不能使用;雖然使用起來方便,但實際上都可以由獲取值的Value函數(shù)代替
{
LOCK(m_Mutex);
return m_Data;
}
JSafe& operator =(const JSafe& other)
{
LOCK(other);
{
LOCK(m_Mutex);
m_Data = other.m_Data;
}
return *this;
}
void Value(const _Tp& other)//設(shè)置值
{
LOCK(m_Mutex);
m_Data = other;
}
const _Tp Value(void) const //獲取值
{
LOCK(m_Mutex);
return m_Data;
}
void Lock(void) const //加鎖
{
m_Mutex.Lock();
}
void Unlock(void) const //解鎖
{
m_Mutex.Unlock();
}
_Tp& Data(void) //加鎖后方可安全使用此函數(shù)得到資源的引用,如未加鎖直接調(diào)用可能造成相應(yīng)的安全問題
{
return m_Data;
}
protected:
_Tp m_Data; //數(shù)據(jù)成員
mutable _Mutex m_Mutex; //鎖成員
friend class JLockHelper;
};
/**************************************************************************************
* JLockHelper 使用于有Lock,及Unlock成員函數(shù)的安全類或鎖類的輔助加鎖對象,
* 可以做到異常安全的解鎖
* 說明: 在創(chuàng)建類時加鎖,析構(gòu)時解鎖;一種簡單的但不是必須的使用方法是使用{}
* 將要加鎖的代碼括起來,在{}中第一行定義該輔助類,則}時將自動解鎖
****************************************************************************************/
class JLockHelper
{
public:
JLockHelper(const JAbsXMutex& xMutex):m_xMutex(xMutex)
{
m_xMutex.Lock();
}
template<class _Tp,class _Mutex>
JLockHelper(const JSafe<_Tp,_Mutex>& S):m_xMutex(S.m_Mutex)
{
m_xMutex.Lock();
}
~JLockHelper()
{
m_xMutex.Unlock();
}
private:
const JAbsXMutex& m_xMutex;
};
template<class _Tp>
class JSafeNum : public JSafe<_Tp>
{
public:
typedef JSafe<_Tp> safe_type;
explicit JSafeNum(const _Tp& _Value = 0):JSafe<_Tp>(_Value){}
JSafeNum& operator ++()
{
LOCK(safe_type::m_Mutex);
safe_type::m_Data++;
return *this;
}
JSafeNum operator ++(int)
{
LOCK(safe_type::m_Mutex);
JSafeNum tmp(safe_type::m_Data++);
return tmp;
}
JSafeNum& operator +=(const _Tp& other)
{
LOCK(safe_type::m_Mutex);
safe_type::m_Data += other;
return *this;
}
JSafeNum& operator --(void)
{
LOCK(safe_type::m_Mutex);
safe_type::m_Data--;
return *this;
}
JSafeNum operator --(int)
{
LOCK(safe_type::m_Mutex);
JSafeNum tmp(safe_type::m_Data--);
return tmp;
}
JSafeNum& operator -=(const _Tp& other)
{
LOCK(safe_type::m_Mutex);
safe_type::m_Data -= other;
return *this;
}
JSafeNum& operator =(const _Tp& other)
{
LOCK(safe_type::m_Mutex);
safe_type::m_Data = other;
return *this;
}
};
typedef JSafe<bool> JSafeBool;
typedef JSafeNum<int> JSafeInt;
typedef JSafeNum<long> JSafeLong;
typedef JSafeNum<u_int> JSafeUint;
//安全容器,適用于stl容器類
template<class _Con>
class JSafeCon :public JSafe<_Con>
{
public:
typedef JSafe<_Con> _Base;
typedef JSafeCon<_Con> _Self;
typedef _Con con_type;
typedef typename con_type::iterator iterator;
explicit JSafeCon(void):_Base(){}
bool empty()
{
LOCK(_Base::m_Mutex);
return _Base::m_Data.empty();
}
size_t size()
{
LOCK(_Base::m_Mutex);
return _Base::m_Data.size();
}
void swap(con_type& Seq)
{
LOCK(_Base::m_Mutex);
_Base::m_Data.swap(Seq);
}
//以下兩個函數(shù)及其使用均需在同步的保護(hù)下
iterator begin()
{
return _Base::m_Data.begin();
}
iterator end()
{
return _Base::m_Data.end();
}
};
//安全序列(適用于stl的序列容器類如vector,list,...)
template<class _Sequence>
class JSafeSeq : public JSafeCon<_Sequence>
{
public:
typedef JSafeCon<_Sequence> _Base;
typedef JSafeSeq<_Sequence> _Self;
typedef _Sequence seq_type;
typedef typename seq_type::iterator iterator;
typedef typename seq_type::value_type value_type;
explicit JSafeSeq(void):_Base(){}
void push_back(const value_type& Val)
{
LOCK(_Base::m_Mutex);
_Base::m_Data.push_back(Val);
}
};
//適用于stl前向插入序列如deque等
template<class _FSeq>
class JSafeFSeq:public JSafeSeq<_FSeq>
{
public:
typedef JSafeSeq<_FSeq> _Base;
typedef JSafeFSeq<_FSeq> _Self;
typedef typename _Base::seq_type seq_type;
typedef typename seq_type::iterator iterator;
typedef typename seq_type::value_type value_type;
explicit JSafeFSeq(void):_Base(){}
bool pop_front(value_type& Val)//取出第一個元素
{
LOCK(_Base::m_Mutex);
if (_Base::m_Data.empty())
{
return false;
}
else
{
Val = *(_Base::m_Data.begin());
_Base::m_Data.pop_front();
return true;
}
}
};
//安全map類,適用于stl::map
template<class _Map>
class JSafeMap :public JSafeCon<_Map>
{
public:
typedef JSafeCon<_Map> _Base;
typedef JSafeSeq<_Map> _Self;
typedef _Map map_type;
typedef typename map_type::iterator iterator;
typedef typename map_type::const_iterator const_iterator;
typedef typename map_type::value_type value_type;
typedef typename value_type::first_type key_type;
typedef typename value_type::second_type data_type;
explicit JSafeMap(void):_Base(){}
void insert(const value_type& Val)
{
LOCK(_Base::m_Mutex);
_Base::m_Data.insert(Val);
}
bool find(const key_type& Key) const
{
LOCK(_Base::m_Mutex);
return _Base::m_Data.find(Key) != _Base::m_Data.end();
}
bool find(const key_type& Key,data_type& Val) const
{
LOCK(_Base::m_Mutex);
const_iterator it = _Base::m_Data.find(Key);
if ( _Base::m_Data.end() != it )
{
Val = it->second;
return true;
}
return false;
}
void erase(const key_type& Key)
{
LOCK(_Base::m_Mutex);
_Base::m_Data.erase(Key);
}
data_type get(const key_type& Key,const data_type& Default) const
{
LOCK(_Base::m_Mutex);
const_iterator it = _Base::m_Data.find(Key);
if ( _Base::m_Data.end() == it )
{
return Default;
}
else
{
return it->second();
}
}
bool find_erase(const key_type& Key,data_type& Val)
{
LOCK(_Base::m_Mutex);
iterator it = _Base::m_Data.find(Key);
if ( _Base::m_Data.end() != it )
{
Val = it->second;
_Base::m_Data.erase(it);
return true;
}
return false;
}
void set(const key_type& Key,const data_type& Val)
{
LOCK(_Base::m_Mutex);
_Base::m_Data[Key] = Val;
}
};
#endif //head file
/*
* =====================================================================================
*
* Filename: mtmatch.cpp
*
* Description: 多線程的匹配工具,類似“grep vollin vollin.txt”
*
* Version: 1.0
* Created: 04/30/2009 01:11:07 PM CST
* Revision: none
* Compiler: g++
*
* Author: vollinwan
* Company:
*
* =====================================================================================
*/
#include "safe.h"
#include <deque>
#include <list>
#include <string>
#include <iostream>
#include <fstream>
using namespace std;
JSafeFSeq<deque<string*> > g_HandleDeque; //待處理隊列
JSafeSeq<list<string*> > g_ResList; //結(jié)果列表
JSafeBool g_bPutOver(false); //輸入結(jié)束標(biāo)志
JSafeInt g_nCurTCnt(0); //當(dāng)前運行的工作線程數(shù)
string g_sFilter; //過濾字符串
const u_int g_uTcnt = 3; //線程數(shù)
inline u_int usleep_r(u_int usec)
{
//usleep的線程安全版本
struct timespec rqtp;
struct timespec rem;
memset(&rem,0,sizeof(rem));
rqtp.tv_sec = usec/1000000;
rqtp.tv_nsec = usec%1000000*1000;
nanosleep(&rqtp, &rem);
return rem.tv_sec * 1000000 + rem.tv_nsec / 1000;
}
//工作線程的主函數(shù)
void* Work(void* pParam)
{
++g_nCurTCnt;
string* pIn=NULL;
while (1)
{
while (g_HandleDeque.pop_front(pIn))
{
if (pIn->find(g_sFilter) != string::npos)
{
g_ResList.push_back(pIn);
}
else
{
delete pIn;
}
}
if (g_bPutOver)
{
break;
}
else
{
usleep_r(1);
}
}
--g_nCurTCnt;
return NULL;
}
int main(int argc,char** argv)
{
string sUsage = string(argv[0]) + " filter filepath";
if (argc != 3)
{
cout<<sUsage<<endl;
_exit(1);
}
g_sFilter = argv[1];
ifstream f(argv[2]);
if ( !f )
{
cout<<argv[2]<<" can't open!"<<endl;
_exit(1);
}
pthread_t tI[g_uTcnt];
for (int i=0;i<g_uTcnt;i++)
{
if (0 != pthread_create(&tI[i],NULL,Work,NULL) )
{
cout<<"congratulations!"<<endl;
_exit(1);
}
}
string *pLine = new string;
while(getline(f,*pLine))
{
g_HandleDeque.push_back(pLine);
pLine = new string;
}
delete pLine;
g_bPutOver=true;
while (1)
{
list<string*> l;
if (!g_ResList.empty())
{
g_ResList.swap(l);
for (list<string*>::iterator it = l.begin();it != l.end();)
{
cout<<**it<<endl;
delete *it;
it = l.erase(it);
}
}
else if ( g_HandleDeque.empty() && g_nCurTCnt == 0)
{
break;
}
else
{
usleep_r(1);
}
}
return 0;
}
[vollin1]因本文內(nèi)容并不涉及并行與并發(fā)相關(guān)區(qū)別所以文章后續(xù)提到的并發(fā)均指并行或并發(fā)。
[vollin2]本文不討論庫的線程安全問題,假定所使用的庫均是線程安全版本
[vollin3]雖然也可以用一個鎖來管理所有共享數(shù)據(jù)的同步,但無疑這樣做是很低效的,所以每組共享數(shù)據(jù)與一個鎖一一對應(yīng)是才是正常的同步方式。
[vollin4]此處使用大括號,而未使用生命周期這一字樣是為了強(qiáng)調(diào)大括號這種使用方式。雖然兩者的內(nèi)涵完全一致。