DCLP單例實(shí)現(xiàn)的典型代碼如下:
static A *getInstance()
{
if(local_instance == nullptr){
pthread_mutex_lock(&mutex);
if (local_instance == nullptr)
{
local_instance = new A();
}
pthread_mutex_unlock(&mutex);
}
return local_instance;
}
網(wǎng)上有一種說(shuō)法盏触,local_instance = new A()這句話是有風(fēng)險(xiǎn)的蔬蕊。因?yàn)橛锌赡苤噶畎聪旅娴捻樞驁?zhí)行:
- 為A申請(qǐng)內(nèi)存
- 內(nèi)存首地址賦給local_instance
- 在內(nèi)存中構(gòu)造A
所以在一個(gè)多線程應(yīng)用中,有可能A線程剛執(zhí)行完2奕枝,還沒(méi)來(lái)得及執(zhí)行3棺榔,另外一個(gè)線程取到了非空但未構(gòu)造的local_instance。
之前一直對(duì)這個(gè)說(shuō)法有懷疑隘道,覺(jué)得鉆牛角尖了症歇。今天在一臺(tái)Arm64機(jī)器上做了測(cè)試,代碼如下:
#include <thread>
#include <gtest/gtest.h>
class A;
A* local_instance = nullptr;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
class A
{
private:
A(void)
{
v = 12345678;
}
public:
int v;
static A *getInstance()
{
if(local_instance == nullptr)
{
pthread_mutex_lock(&mutex);
if (local_instance == nullptr)
{
local_instance = new A();
}
pthread_mutex_unlock(&mutex);
}
return local_instance;
}
};
TEST(test_memory_order, TEST001)
{
std::thread t1([]()
{
assert(A::getInstance()->v == 12345678);
});
std::thread t2([]()
{
assert(A::getInstance()->v == 12345678);
});
t1.join();
t2.join();
delete ::local_instance;
::local_instance = nullptr;
}
int main(int argc, char* argv[])
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
編譯:g++ a.cpp -lpthread -lgtest -O2
運(yùn)行:./a.out --gtest_repeat=100000
果然會(huì)報(bào)錯(cuò):
雖然問(wèn)題可以復(fù)現(xiàn)谭梗,但我個(gè)人并不認(rèn)同網(wǎng)上的說(shuō)法忘晤,“構(gòu)造A”和“首地址賦給local_instance"發(fā)生了重排。在A::A()沒(méi)有內(nèi)聯(lián)的情況下激捏,一條是bl指令设塔,一條是store指令。如果強(qiáng)制A::A()不可內(nèi)聯(lián):
A(void)__attribute__((noinline))
線程函數(shù)體匯編代碼如下(開(kāi)O2編譯缩幸,getInstance被內(nèi)聯(lián)展開(kāi)了)
那么壹置,按網(wǎng)上的說(shuō)法,是下面這兩條指令發(fā)生了重排:
9184 991c: 94000065 bl 9ab0 <_ZN1AC1Ev>
9185 9920: f9001e74 str x20, [x19, #56]
不論是編譯器還是CPU表谊,都不應(yīng)該重排bl和store钞护。因?yàn)椴还茉趺粗嘏牛紤?yīng)保證單線程執(zhí)行結(jié)果不變爆办。例如难咕,萬(wàn)一bl的目標(biāo)函數(shù)拋出了異常,本來(lái)store指令是執(zhí)行不到的距辆,現(xiàn)在把store提到前面余佃,不就違背了單線程執(zhí)行結(jié)果不變的底線嗎?
既然bl和store不能重排跨算,上面的assert又為什么會(huì)失敗呢爆土?
我認(rèn)為原因是,v = 12345678是一條store指令诸蚕,給local_instance賦值也是一條store指令步势。在arm64上,因?yàn)閟tore buffer背犯、invalidate queue的原因坏瘩,發(fā)生了store store的memory reordering,導(dǎo)致在另外一個(gè)CPU看來(lái)漠魏,給local_instance賦值比給v賦值先發(fā)生了倔矾。
為了驗(yàn)證這個(gè)說(shuō)法,修改A::A()如下:
A(void)__attribute__((noinline))
{
v = 12345678;
__asm__ __volatile__("dmb ishst" : : : "memory"); //即linux kernel中的__smp_wmb
}
如果按網(wǎng)上的說(shuō)法,bl和store發(fā)生了重排哪自,那么不管A::A()是怎么定義的丰包,都會(huì)發(fā)生先給local_instance賦值后給v賦值的情況。然而提陶,這樣修改后烫沙,測(cè)試100W次,都沒(méi)有assert發(fā)生隙笆。