第三節(jié) 線程互斥
本節(jié)介紹如下內(nèi)容
1.
主動(dòng)對(duì)象
2.
調(diào)度與原子操作
3.
競(jìng)爭(zhēng)條件和數(shù)據(jù)一致性
4.
為何需要互斥
5.
互斥類接口定義
6.
示例程序
7.
互斥類的Unix和Windows實(shí)現(xiàn)
1. 主動(dòng)對(duì)象(Active Object)
第二節(jié)介紹Thread類的時(shí)候曾經(jīng)提到,每個(gè)子線程一旦被創(chuàng)建,就被賦予了自己的生命。當(dāng)一個(gè)線程類創(chuàng)建并啟動(dòng)之后,它將會(huì)以自己的步調(diào)主動(dòng)執(zhí)行其獨(dú)立線程,它和其他線程(包括主線程)的執(zhí)行是并行關(guān)系。
為了詳細(xì)說(shuō)明這個(gè)問(wèn)題,先介紹一下什么是控制流(control
flow)。計(jì)算機(jī)科學(xué)中,控制流指指令式(imperative)或函數(shù)式(functional)編程語(yǔ)言中語(yǔ)句、指令、函
數(shù)調(diào)用的執(zhí)行(或求值)序列。
單線程程序中,控制流始終是一線串珠,一旦控制流達(dá)到某個(gè)對(duì)象,由程序主動(dòng)調(diào)用對(duì)象函數(shù),在函數(shù)執(zhí)行完畢后,控制流返回主程序繼續(xù)執(zhí)行。對(duì)于被訪問(wèn)對(duì)象來(lái) 說(shuō),訪問(wèn)、修改、成員函數(shù)調(diào)用都是一個(gè)被動(dòng)的過(guò)程,此過(guò)程受控于該程序的控制流。所以,稱單線程程序的對(duì)象為被動(dòng)對(duì)象(Passive Object)。
與被動(dòng)對(duì)象相對(duì)應(yīng)的是主動(dòng)對(duì)象。主動(dòng)對(duì)象和被動(dòng)對(duì)象的區(qū)別在于,主動(dòng)對(duì)象可以發(fā)起自己的線程,創(chuàng)建新的執(zhí)行序列,產(chǎn)生獨(dú)立于主線程的控制流分支。簡(jiǎn)而言 之,主動(dòng)對(duì)象可以獨(dú)立于主線程,自主制定自己的控制流。如果愿意,你可以實(shí)現(xiàn)一個(gè)和主線程沒(méi)有任何協(xié)調(diào)關(guān)系,天馬行空式的獨(dú)立線程。當(dāng)然,這樣的線程往往 也意味著毫無(wú)用處(娛樂(lè)作用除外)。
2. 調(diào)度和原子操作
從理論模型上說(shuō),多線程環(huán)境中,線程之間是獨(dú)立、對(duì)等關(guān)系。一個(gè)線程完全可以無(wú)視其他線程的存在。但實(shí)際多線程執(zhí)行環(huán)境中,線程數(shù)往往遠(yuǎn)大于CPU數(shù)量。 為了共享和分配資源,線程必需遵守一定規(guī)則,進(jìn)行必要的協(xié)調(diào)。操作系統(tǒng)中具體的規(guī)則執(zhí)行者是調(diào)度程序。因此,要想掌握多線程編程,就必需了解線程調(diào)度的基 本概念和特點(diǎn)。在此基礎(chǔ)上,能了解為什么需要在線程之間進(jìn)行協(xié)調(diào),進(jìn)而才能透徹理解如何協(xié)調(diào)多線程的并發(fā)執(zhí)行。
現(xiàn)代操作系統(tǒng)中,存在許多不同的線程調(diào)度模型,這些調(diào)度模型的基本共同點(diǎn)就是線程執(zhí)行順序具有隨機(jī)性和不確定性。調(diào)度模型既無(wú)法事先知道某一時(shí)刻會(huì)存在多 少線程,也無(wú)法知道哪個(gè)線程會(huì)正在運(yùn)行,甚至也不知道某個(gè)線程確切會(huì)到什么時(shí)刻執(zhí)行結(jié)束,更不用說(shuō)預(yù)先安排在特定時(shí)刻執(zhí)行特定線程的特定語(yǔ)句。
前面提到,控制流就是語(yǔ)句,指令或函數(shù)調(diào)用的順序序列。反映到時(shí)間軸上,就是一系列離散分布的執(zhí)行序列。線程調(diào)度作用于多個(gè)控制流的客觀效果,就是多個(gè)控 制流按同一條時(shí)間軸排列時(shí),是一個(gè)近似隨機(jī)的執(zhí)行序列;按每個(gè)控制流各自的時(shí)間軸排列時(shí),則是一個(gè)具有先后順序的執(zhí)行序列。
在具體的程序執(zhí)行上,這個(gè)特點(diǎn)就表現(xiàn)為線程A的一個(gè)語(yǔ)句執(zhí)行完畢后,緊接著執(zhí)行的可能是另一個(gè)線程B的一個(gè)語(yǔ)句。甚至可能是線程A的一個(gè)語(yǔ)句還沒(méi)有執(zhí)行完畢,就接著執(zhí)行線程B的語(yǔ)句。
對(duì)于后一點(diǎn)不用感到奇怪。因?yàn)椴僮飨到y(tǒng)中,調(diào)度程序作為系統(tǒng)底層實(shí)現(xiàn)的一部分,參與調(diào)度的操作指令可能比高級(jí)編程語(yǔ)言的基本語(yǔ)句要底層的多。同一個(gè)調(diào)度程 序,其調(diào)度的眾多線程可能由多種高級(jí)語(yǔ)言編寫,所以調(diào)度程序基本上不可能以某種高級(jí)編程語(yǔ)言的單條語(yǔ)句為單位安排執(zhí)行序列。
通常,一條高級(jí)語(yǔ)言語(yǔ)句可能對(duì)應(yīng)多條匯編指令,而一條匯編指令可能對(duì)應(yīng)多條CPU微碼指令。而一條微碼指令,則可能對(duì)應(yīng)邏輯電路的一個(gè)具體電路邏輯。順便 提一下,這也是Verilog, VHDL等高級(jí)語(yǔ)言能夠綜合出具體的邏輯電路的基本原理。所不同的是,高級(jí)語(yǔ)言編譯器編譯的最終單位是匯編,而硬件描述語(yǔ)言綜合的最終單位是和微碼對(duì)應(yīng)的 電路器件單元(集成電路前端設(shè)計(jì)的內(nèi)容。記得以前暑期實(shí)習(xí),我窩在學(xué)校做的就是這么一個(gè)元件庫(kù),做的很不像樣子居然還得到了一定的肯定,多年后想起來(lái)還是 汗顏)。
至于系統(tǒng)調(diào)度程序具體會(huì)定義怎樣的原子操作集合,會(huì)以什么粒度的指令為調(diào)度基本單位,這就是系統(tǒng)設(shè)計(jì)者各顯神通的地方了。個(gè)人認(rèn)為,作為高級(jí)語(yǔ)言的編程 者,記住什么操作是原子操作意義并不明顯。大多數(shù)場(chǎng)合,只要認(rèn)為,多線程的控制流在同一條時(shí)間軸上看來(lái)是完全隨機(jī)的就可以了。要記住,墨菲定律生效的時(shí) 候,看似不可能的事情都能成為現(xiàn)實(shí)。
多線程程序設(shè)計(jì)中,一種樸素的設(shè)計(jì)思想是將線程具體化為主動(dòng)對(duì)象,主動(dòng)對(duì)象之間通過(guò)共享環(huán)境(全局資源)來(lái)維護(hù)當(dāng)前應(yīng)用的運(yùn)行狀態(tài);主動(dòng)對(duì)象間通過(guò)一套約 定的互斥和同步規(guī)則來(lái)實(shí)現(xiàn)通信;線程的具體執(zhí)行時(shí)序則由調(diào)度程序安排。主動(dòng)對(duì)象之間,應(yīng)盡量避免一個(gè)主動(dòng)對(duì)象直接調(diào)用另一個(gè)主動(dòng)對(duì)象的內(nèi)部方法(例如 suspend, stop, exit)直接控制另一個(gè)線程的執(zhí)行步調(diào)。主動(dòng)對(duì)象的行為應(yīng)該完全由其本身控制,和其他主動(dòng)對(duì)象的交互盡量只通過(guò)環(huán)境以及同步語(yǔ)句來(lái)實(shí)現(xiàn)。
實(shí)際實(shí)現(xiàn)中,如果多個(gè)主動(dòng)對(duì)象都可以直接調(diào)用某個(gè)主動(dòng)對(duì)象的stop方法終止其運(yùn)行,這個(gè)程序是非常脆弱的,因?yàn)樵谡{(diào)度程序之外,不借助同步機(jī)制,主觀假 定線程執(zhí)行時(shí)序,人為更改線程執(zhí)行狀態(tài)是非常不明智的。即使這樣的程序可用,對(duì)于維護(hù)者來(lái)說(shuō)其難度也不亞于維護(hù)goto語(yǔ)句。
這也就是前一篇文章所定義線程類中detach, start, stop沒(méi)有加鎖的原因,因?yàn)椴⒉幌M诙鄠€(gè)線程中調(diào)用同一個(gè)線程對(duì)象的這些方法。
3.
競(jìng)爭(zhēng)條件和數(shù)據(jù)一致性
共享環(huán)境是進(jìn)程全局資源的同義詞。在多線程并發(fā)執(zhí)行中,最常遇到的問(wèn)題就是共享環(huán)境被污染。具體體現(xiàn)就是全局?jǐn)?shù)據(jù)被破壞,全局文件內(nèi)容被破壞 …
。
例如:
有一個(gè)64位全局變量long globalVar = 0;主動(dòng)對(duì)象A想把它置為0x000A000B000C000D;假設(shè)這個(gè)操作分兩步執(zhí)行,先將高32位置為000A000B,再把低32位置為
000C000D。但不巧的是,對(duì)象A剛剛將高位置位,調(diào)度程序安排另一主動(dòng)對(duì)象B執(zhí)行。這時(shí),全局變量globalVar內(nèi)部的值是一個(gè)非法值,它是無(wú) 意義的。在B拿著這個(gè)值做進(jìn)一步處理時(shí),得出的也是錯(cuò)誤結(jié)果。這時(shí),稱為數(shù)據(jù)的一致性被破壞。
線程調(diào)度的不確定性也可能導(dǎo)致程序執(zhí)行結(jié)果的不確定性。有時(shí)候這種不確定性會(huì)導(dǎo)致程序得到錯(cuò)誤的運(yùn)行結(jié)果。
例如:
為了對(duì)Thread類所生成的對(duì)象總數(shù)計(jì)數(shù),定義一個(gè)全局變量Unsigned int counter = 0; 在Thread類的構(gòu)造函數(shù)中,執(zhí)行++counter。現(xiàn)在假設(shè)有2個(gè)Thread對(duì)象objA和objB并發(fā)運(yùn)行,考慮如下兩個(gè)場(chǎng)景:
Scenario1.操作序列如下:
1. counter = 0;
2. objA將counter值0從內(nèi)存讀入寄存器A;
3. objA將寄存器A值加1;
4. objA將寄存器A值寫回內(nèi)存,counter值為1;
5. objB將counter值1從內(nèi)存讀入寄存器B;
6. objB將寄存器B值加1;
7. objA將寄存器B值寫回內(nèi)存,counter值為2;
8. 最終counter值為2。
Scenario2.操作序列如下:
1. counter = 0;
2. objA將counter值0從內(nèi)存讀入寄存器A;
3. objB將counter值0從內(nèi)存讀入寄存器B
4. objA將寄存器A值加1;
5. objB將寄存器B值加1;
6. objA將寄存器A值寫回內(nèi)存,counter值為1;
7. objA將寄存器B值寫回內(nèi)存,counter值為1;
8. 最終counter值為1。
場(chǎng)景1的結(jié)果是設(shè)計(jì)的本意,場(chǎng)景2的結(jié)果對(duì)我們而言是錯(cuò)誤的。
一個(gè)線程的執(zhí)行結(jié)果正確性取決于于其他線程執(zhí)行時(shí)序的條件,稱為競(jìng)爭(zhēng)條件。中文這樣翻譯不倫不類,但從英文字面理解非常容易。Race一般是計(jì)時(shí)賽,某位選手跑得快一點(diǎn),競(jìng)賽結(jié)果就有變化,最終結(jié)果由race condition決定,還是非常形象的。
4. 為何需要互斥
線程間協(xié)作問(wèn)題,通常可以歸為互斥和同步兩類。其中互斥又主要解決兩類問(wèn)題:維護(hù)數(shù)據(jù)一致性、避免競(jìng)爭(zhēng)條件的出現(xiàn)。
解決一致性問(wèn)題,通俗說(shuō)就是,修改數(shù)據(jù)的線程通告其他線程“我正在修改你要訪問(wèn)的對(duì)象X,操作過(guò)程中不能保證這個(gè)數(shù)據(jù)的有效性,請(qǐng)不要使用此對(duì)象”。
避免競(jìng)爭(zhēng)條件,通俗說(shuō)就是,某線程通告其他線程“我將要進(jìn)行涉及某對(duì)象的一系列操作A,在我未完成這一系列操作之前,如果有人和我同時(shí)執(zhí)行涉及此對(duì)象的操作序列B(B也可能就是A),將會(huì)影響我執(zhí)行結(jié)果的正確性,請(qǐng)不要進(jìn)行涉及此對(duì)象的操作”。
這種操作序列A有時(shí)候也被稱為“原子性操作”,因?yàn)樗辉试S操作序列B在時(shí)間軸上和它交叉分布,必需保證在時(shí)間軸上看來(lái),操作序列A是一個(gè)不可分割的整體。(物理學(xué)早期,原子也被認(rèn)為是不可分割的)。
以上冗長(zhǎng)的解釋,精簡(jiǎn)成一句話,就是“線程間需要互斥執(zhí)行”。需要互斥的操作對(duì)應(yīng)的代碼也有一個(gè)很學(xué)術(shù)的名稱-“關(guān)鍵域(或關(guān)鍵區(qū))”。
那么如何實(shí)現(xiàn)互斥呢?一個(gè)簡(jiǎn)單的思路就是,設(shè)立一個(gè)權(quán)威仲裁者,給那些需要互斥執(zhí)行的線程頒發(fā)一個(gè)共用的通行證。某個(gè)線程想要進(jìn)入一個(gè)關(guān)鍵域執(zhí)行,需要先 申請(qǐng)可以進(jìn)入該關(guān)鍵域的通行證,如果別的線程已經(jīng)拿走了該通行證,則本線程等待,進(jìn)入休眠狀態(tài),退出調(diào)度。如果本線程的通行證使用完畢,則應(yīng)該將它歸還給 仲裁者,重新喚醒等待線程,參與調(diào)度,競(jìng)爭(zhēng)此通行證。
比如,下列偽碼中,threadFuncA和threadFuncB就需要申請(qǐng)同一張通行證:
例一:
int globalCounter = 0;
void threadFuncA (通行證類型
* 通行證)
{
獲取通行證;
globalCounter++;
歸還通行證;
}
Void threadFuncB (通行證類型
* 通行證)
{
獲取通行證;
globalCounter *= 2;
歸還通行證;
}
又比如,下列偽碼中,需要為ResourceClass類的對(duì)象引用計(jì)數(shù)器制定一張通行證
。
例二:
class ResourceClass
{
public:
resource & reference()
{
獲取通行證;
++refcounter;
printf(“當(dāng)前對(duì)象被引用了%u次”, refCounter);
釋放通行證;
}
private:
通行證類型
通行證;
unsigned int refCounter;
};
ResourceClass rescObj
Void threadFuncA()
{
rescObj-> reference();
}
Void threadFuncB()
{
rescObj-> reference();
}
最后一個(gè)例子,是為ResourceClass類的對(duì)象計(jì)數(shù)器制定一張通行證。
例三:
class ResourceClass
{
public:
ResourceClass ()
{
獲取通行證;
++objcounter;
printf(“當(dāng)前類創(chuàng)建了%u個(gè)對(duì)象”, objCounter);
釋放通行證;
}
private:
static通行證類型
通行證;
unsigned int objCounter;
};
Void threadFuncA()
{
ResourceClass * rescObj = new ResourceClass ();
}
Void threadFuncB()
{
ResourceClass * rescObj = new ResourceClass ();
}
這三個(gè)例子中,例一是不同函數(shù)之間互斥,所以通行證的作用域要對(duì)兩個(gè)函數(shù)都可見(jiàn)。例二是同一個(gè)對(duì)象的內(nèi)部函數(shù)多次調(diào)用之間的互斥,所以只要保證該函數(shù)多次 調(diào)用時(shí)共用的都是當(dāng)前對(duì)象內(nèi)部同一份通行證即可。例三是同一個(gè)類的所有對(duì)象在創(chuàng)建時(shí)都要互斥,所以必需保證這個(gè)類的所有對(duì)象構(gòu)造時(shí)共用的時(shí)同一份通行證, 從而通行證被聲明為靜態(tài)成員。
這里所說(shuō)的“通行證”在多線程編程中對(duì)應(yīng)一個(gè)專門的術(shù)語(yǔ)mutex,由“mutual exclusion”拼接而來(lái)。為什么不直接用“鎖”的概念呢?因?yàn)?#8220;鎖”并不能很好的表達(dá)互斥的含義。鎖是指一定條件下不允許當(dāng)前代碼段執(zhí)行的概念。如 上述例二或例三,不允許多個(gè)線程同時(shí)執(zhí)行同一函數(shù),這時(shí)說(shuō)這個(gè)函數(shù)被鎖定是很形象。但在例一中,A函數(shù)被鎖定,為什么B函數(shù)不能執(zhí)行呢?這就較難理解了。
而且經(jīng)常有人感到疑惑,為什么“加鎖后,被鎖定的關(guān)鍵域只能串行執(zhí)行”。這個(gè)其實(shí)是指在各自的時(shí)間軸上,并行的控制流在經(jīng)過(guò)互斥執(zhí)行的代碼段時(shí),必需以先 后順序串行執(zhí)行。在今后的介紹中,mutex的申請(qǐng),用acquire()操作表示,mutex的歸還,用release()表示。舍棄lock(), unlock()的表示。
為了深入理解,先來(lái)看一段使用忙等待實(shí)現(xiàn)互斥的代碼,用的是系統(tǒng)內(nèi)核中使用較多的“spin
lock”互斥方法。
例4.忙等待實(shí)現(xiàn)互斥
//聲明為volatile,防止被編譯器優(yōu)化。
volatile bool dataProcessNotDone = true;
int criticalData = 0;
unsigned threadFuncA( void* para )
{
//如果編譯器不支持volatile關(guān)鍵字,
//打開優(yōu)化選項(xiàng)時(shí),此句可能直接變成死循環(huán)。
while (dataProcessNotDone); // spin lock,鎖定的是后續(xù)數(shù)據(jù)
//被鎖定的代碼區(qū)
printf("critical data is %d\n", CriticalData);
return 0;
}
unsigned threadFuncB( void* para )
{
sleep(1000);
criticalData++;
dataProcessNotDone = false; //修改互斥變量
return 0;
}
在高級(jí)語(yǔ)言中,利用spin lock實(shí)現(xiàn)復(fù)雜互斥條件非常困難,單單處理競(jìng)爭(zhēng)條件就令人望而生畏。Spin lock在每次等待解鎖的時(shí)間都很短時(shí),具有無(wú)需線程切換,無(wú)需再調(diào)度等獨(dú)特優(yōu)勢(shì)。但是在絕大多數(shù)應(yīng)用中,由于互斥等待時(shí)間不確定(可能很長(zhǎng)),多個(gè)線程等待spin lock解鎖的過(guò)程中,spinning的行為可能導(dǎo)致系統(tǒng)處于半癱瘓狀態(tài),會(huì)嚴(yán)重影響程序性能。
除了忙等待之外,很多操作系統(tǒng)或線程庫(kù)都提供了互斥原語(yǔ)來(lái)實(shí)現(xiàn)互斥。如果有可能,應(yīng)當(dāng)盡量使用系統(tǒng)提供的接口實(shí)現(xiàn)互斥。否則,要考慮編譯器優(yōu)化中的常量代入,語(yǔ)句執(zhí)行順序重排,cpu指令序列優(yōu)化等依賴于具體軟硬件環(huán)境的復(fù)雜問(wèn)題(關(guān)鍵是這樣的付出沒(méi)有太大意義)。
下面根據(jù)上述概念,抽象出互斥類的概念,定義如下Mutex類接口
5. 互斥類接口定義
文件mutex.h
#ifndef __MUTEX_H__
#define __MUTEX_H__
// C++標(biāo)準(zhǔn)中以_或__開頭的名字不符合標(biāo)準(zhǔn),
// 這一特點(diǎn)可以讓這樣定義的宏不會(huì)錯(cuò)誤覆蓋其他名字。
class Mutex
{
public:
Mutex();
~Mutex();
bool acquire (bool block = true);
void release();
private:
//依賴于具體實(shí)現(xiàn),后面再說(shuō)。
};
#endif
其中,
Mutex::acquire(),獲取互斥量,有阻塞和非阻塞兩種調(diào)用方式,阻塞方式,獲取互斥量失敗時(shí)線程休眠。非阻塞方式下,獲取失敗時(shí)直接返回。
Mutex::release(),釋放互斥量。
6.
示例程序
下面的例子說(shuō)明了如何實(shí)現(xiàn)多線程環(huán)境下的Singleton模式。
文件Singleton.h:
#ifndef __SINGLETON_H__
#define __SINGLETON_H__
#include <stdio.h>
#include "thread.h"
#include "mutex.h"
// Dummy class.
class Helper {};
// A wrapper class for Mutex class
// It is exception safe.
class Guard
{
public:
Guard(Mutex & lock):mutex(lock)
{
mutex.acquire();
}
~Guard()
{
mutex.release();
}
private:
Mutex & mutex;
};
// Correct but possibly expensive multithreaded version
class Singleton1
{
public:
Helper * getInstance() {
Guard guard(mutex);
if (helper == NULL)
{
helper = new Helper();
}
return helper;
}
private:
static Mutex mutex;
static Helper * helper;
};
// Broken multithreaded version
// "Double-Checked Locking" idiom
class Singleton2
{
public:
Helper * getInstance() {
if (helper == NULL)
{
Guard guard(mutex);
if (helper == NULL)
{
helper = new Helper();
}
}
return helper;
}
private:
static Mutex mutex;
static Helper * helper;
};
//Thread class for test.
template <typename T>
class TestThread: public Thread
{
public:
TestThread<typename T>(T & resource, Helper *&
res, Helper *&
res2Cmp)
:singleton(resource), instance(res), instance2Cmp(res2Cmp)
{}
protected:
void * run (void *)
{
for (int i=0; i<100000; i++)
{
instance = singleton.getInstance();
if (instance != instance2Cmp
&& instance != NULL
&&instance2Cmp != NULL
)
{
printf("Fail! %p <> %p.\n",
instance, instance2Cmp);
}
}
return NULL;
}
private:
T & singleton;
Helper * & instance;
Helper * & instance2Cmp;
};
#endif
文件main.cpp
#include <stdio.h>
#include "singleton.h"
#define SINGLETON Singleton1
Mutex SINGLETON::mutex;
Helper * SINGLETON::helper = NULL;
int main(int argc, char** argv)
{
Helper * instance1= NULL;
Helper * instance2 = NULL;
SINGLETON singleton;
TestThread<SINGLETON> thread1(singleton, instance1,
instance2);
TestThread<SINGLETON> thread2(singleton, instance2,
instance1);
thread1.start();
thread2.start();
thread1.wait();
thread2.wait();
printf("Finished!\n");
return 0;
}
對(duì)此示例程序,說(shuō)明如下幾點(diǎn)。
1.定義了一個(gè)新的Guard類,這樣做的好處是做到異常安全。比如:
try
{
Mutex mutex;
mutex.acquire();
// Some operations
if (errorHappened)
throw Exception();
mutex.release();
}
catch (Exception & e)
{
// Print error message;
}
這段代碼中,拋出異常時(shí),互斥量不能釋放,容易造成死鎖。使用Guard類重寫,
可以實(shí)現(xiàn)異常安全:
try
{
Mutex mutex;
Guard guard(mutex);
// Some operations
if (errorHappened)
throw Exception();
}
catch (Exception & e)
{
// Print error message;
}
2.Singleton1的實(shí)現(xiàn)可以確保多線程安全。但它是一個(gè)低效實(shí)現(xiàn)。假如有100次訪問(wèn),只有1次會(huì)修改instance指針,其余99次都是只讀操作。但是每次訪問(wèn)都需要進(jìn)行互斥量的獲取和釋放操作。
取決于系統(tǒng)實(shí)現(xiàn)方式,互斥操作可能比整型變量的++操作慢一個(gè)數(shù)量級(jí)。有的實(shí)現(xiàn)提供的mutex其實(shí)是進(jìn)程級(jí)別的互斥,一次互斥操作,會(huì)進(jìn)入內(nèi)核態(tài),然后再返回用戶態(tài)。而有的線程庫(kù)提供的是Process local mutex,要稍快一些。但無(wú)論那種實(shí)現(xiàn),代價(jià)都較大。
因此,為了改進(jìn)Singleton1的實(shí)現(xiàn)效率,"Double-Checked Locking" idiom被提了出來(lái)。其思路是如果instance指針為空再進(jìn)入互斥操作。由于獲取互斥量過(guò)程中,可能別的線程已經(jīng)將instance指針賦值,所以需要在獲得互斥量所有權(quán)之后,再次檢查instance指針值。這就是所謂”double
check”中double的來(lái)歷。
Double check的設(shè)計(jì)很聰明,但可惜無(wú)論在C++中還是在Java中,這么做其實(shí)都不能保證線程安全。考慮如下序列:
Step1. 線程A檢查instance指針,發(fā)現(xiàn)其為空。獲取互斥量成功,運(yùn)行至語(yǔ)句helper = new Helper();
在打開優(yōu)化選項(xiàng)時(shí),這個(gè)語(yǔ)句在優(yōu)化后可能變成2步子操作, 而且編譯器自動(dòng)調(diào)整了原語(yǔ)句的執(zhí)行順序(reordering):
1) 分配內(nèi)存,將地址賦值給helper變量。此時(shí)helper值非空。
2) 開始初始化此內(nèi)存。
在運(yùn)行完子語(yǔ)句1后,線程發(fā)生切換,此時(shí)內(nèi)存尚未初始化。
Step2. 線程B檢查instance指針,發(fā)現(xiàn)其非空。對(duì)instance所指對(duì)象進(jìn)行進(jìn)一步操作,由于此時(shí)對(duì)象是初始化還未完成無(wú)效數(shù)據(jù),程序崩潰。
那么如何實(shí)現(xiàn)安全的double check呢?vc2005以后的版本,以及java
1.6以后的版本中,可以通過(guò)為helper加上volatile限定符,防止編譯優(yōu)化時(shí)調(diào)整指令執(zhí)行順序。最新的g++對(duì)volatile如何處理,沒(méi) 有查到相關(guān)資料。不過(guò)在C++中,只要本地匯編支持memoryBarrier指令,也可以通過(guò)在C++代碼中內(nèi)嵌匯編指令實(shí)現(xiàn)線程安全。
在此不再詳細(xì)討論。
除此之外,instance類型是否是基本類型,是否多核環(huán)境,都對(duì)不安全的double check版本運(yùn)行結(jié)果有微妙影響。
3.無(wú)論編譯器如何實(shí)現(xiàn),無(wú)論硬件環(huán)境如何,即使它最慢,也應(yīng)該盡量使用系統(tǒng)提供的互斥原語(yǔ)。只有它是能夠確保安全的。通過(guò)系統(tǒng)接口實(shí)現(xiàn)互斥,可以避免考慮編譯優(yōu)化等復(fù)雜情況。一種觀點(diǎn)說(shuō)volatile可以確保上述double
check有效,但是intel有技術(shù)人員專門從硬件的角度批駁了這個(gè)說(shuō)法,他告訴大家,即使編譯器不做這個(gè)reordering, 處理器也可能在指令級(jí)別做。唯一能確保安全的,還是由系統(tǒng)實(shí)現(xiàn)的互斥接口。(照此說(shuō)法,MS 和
Intel結(jié)成wintel聯(lián)盟還是必要的,呵呵)雙方說(shuō)法似乎都一樣權(quán)威時(shí),作為程序開發(fā)者,很難取舍,因此在此類問(wèn)題上還是應(yīng)該適當(dāng)保守。
4.Mutex, volatile變量,普通變量在使用中的具體效率對(duì)比,是一個(gè)非常復(fù)雜的問(wèn)題。涉及到內(nèi)存,緩存,寄存器之間各種情況的同步問(wèn)題。不過(guò)大家可以針對(duì)一些簡(jiǎn)單的例子,測(cè)試一下執(zhí)行時(shí)間上的差異。
7. Unix實(shí)現(xiàn)
下面是借助pthread的Mutex類實(shí)現(xiàn)。
文件Mutex.h
#ifndef __MUTEX_H__
#define __MUTEX_H__
#include <pthread.h>
class Mutex
{
public:
Mutex();
virtual ~Mutex();
virtual bool acquire (bool block = true);
virtual void release();
private:
pthread_mutex_t handle;
};
#endif
文件 Mutex.cpp
#include "mutex.h"
Mutex::Mutex()
{
pthread_mutex_init(&handle, NULL);
}
Mutex::~Mutex()
{
pthread_mutex_destroy(&handle);
}
bool Mutex::acquire(bool block)
{
if (block)
{
return pthread_mutex_lock(&handle) == 0;
}
else
{
return pthread_mutex_trylock(&handle) == 0;
}
}
void Mutex::release()
{
ReleaseMutex(handle);
}
Windows實(shí)現(xiàn)
文件mutex.h
#ifndef __MUTEX_H__
#define __MUTEX_H__
#include <windows.h>
class Mutex
{
public:
Mutex();
virtual ~Mutex();
virtual bool acquire (bool block = true);
virtual void release();
private:
HANDLE handle;
};
#endif
文件mutex.cpp
#include "mutex.h"
Mutex::Mutex()
{
handle = CreateMutex(NULL, false, NULL);
}
Mutex::~Mutex()
{
CloseHandle(handle);
}
bool Mutex::acquire(bool block)
{
//Use caution when calling the wait functions
//and code that directly or indirectly creates windows.
return WaitForSingleObject(handle, block ? INFINITE : 0) ==
WAIT_OBJECT_0;
}
void Mutex::release()
{
ReleaseMutex(handle);
}
小結(jié)
本節(jié)從控制流的角度進(jìn)一步介紹了什么是多線程執(zhí)行中的并發(fā),在此基礎(chǔ)上介紹了主動(dòng)對(duì)象的概念。在多線程編程中,需要考慮線程協(xié)作的地方,如果執(zhí)行順序理不清,為每一個(gè)線程畫一條時(shí)間軸,標(biāo)出各自時(shí)序,對(duì)分析問(wèn)題往往能有幫助。
本節(jié)也介紹了多線程設(shè)計(jì)的一個(gè)基本思想,就是主動(dòng)對(duì)象要具有一定的獨(dú)立性,和其他線程的交互盡量只通過(guò)進(jìn)程環(huán)境、系統(tǒng)或線程庫(kù)提供的同步原語(yǔ)實(shí)現(xiàn)。
為什么要互斥,這個(gè)基本問(wèn)題不能透徹理解的話,鎖的概念很容易把自己弄糊涂。互斥的粒度大小也就根本無(wú)法談起。
互斥的效率問(wèn)題,在實(shí)際設(shè)計(jì)中是一個(gè)值得考慮的問(wèn)題。但是程序的正確性問(wèn)題是一個(gè)更重要的問(wèn)題。不能保證正確性,就不要用。保證正確性到實(shí)現(xiàn)高效性的過(guò)程,類似于學(xué)會(huì)走路到能夠飛奔的過(guò)程。對(duì)于初學(xué)者來(lái)說(shuō),欲速則不達(dá)。
為了對(duì)互斥量的使用,“通行證”所有權(quán)的轉(zhuǎn)換,以及不同系統(tǒng)中Mutex的實(shí)現(xiàn)效率等有一個(gè)充分的感性認(rèn)識(shí),大家請(qǐng)多動(dòng)手實(shí)現(xiàn)才能真正有所收益,最終超越我本人的一己知見(jiàn)。
最后,歡迎提出各種意見(jiàn),以便這個(gè)系列能夠錯(cuò)誤少一些,解釋準(zhǔn)確一些,幫助也更大一些。