C++函數內的靜態變數初始化以及線程安全問題?

有下列測試代碼:

#include & #include &

int create();
int main(int argc, char **argv)
{

std::thread workerB([]() {
std::cout &<&< "B start time: " &<&< std::chrono::system_clock::now().time_since_epoch().count() &<&< std::endl; int b = create(); std::cout &<&< "B: " &<&< b &<&< std::endl; }); std::thread workerA([]() { std::cout &<&< "A start time: " &<&< std::chrono::system_clock::now().time_since_epoch().count() &<&< std::endl; int a = create(); std::cout &<&< "A: " &<&< a &<&< std::endl; }); workerA.join(); workerB.join(); return 0; } int create() { static struct M { int value; M(): value(0) { std::cout &<&< "Create M" &<&< std::endl; std::this_thread::sleep_for(std::chrono::seconds(5)); } } m; std::cout &<&< "time: " &<&< std::chrono::system_clock::now().time_since_epoch().count() &<&< std::endl; return m.value++; }

輸出結果:

B start time: 1518341691213615264
Create M
A start time: 1518341691215440500
time: 1518341696213975008
B: 0
time: 1518341696214035132
A: 1

按理來說, 兩個線程都調用了create函數, 而create函數里又有一個時間比較長的構造函數,static變數的構造函數只會執行一次,那麼兩次create時,肯定有一次沒有執行構造,如果是這樣的話, 後一個進入的create()時候, 為啥既沒有執行構造也變成阻塞態了? 好像這種構造(劃重點, 不知指create()函數)已經是線程安全了, C++是怎麼實現這種線程安全的?


int create()
{
static struct M {
int value;
M(): value(0) {
std::cout &<&< "Create M" &<&< std::endl; std::this_thread::sleep_for(std::chrono::seconds(5)); } } m; return m.value++; }

C++11保證第一句話是線程安全的,函數會使用once_flag和鎖來包裹執行m的構造函數

第二句話不是

是不是線程安全不是用實驗說話的,而是按照標準的文字去檢查你的代碼


C++11 保證靜態局部變數的初始化過程是線程安全的。

Is Meyers implementation of the Singleton pattern thread safe?stackoverflow.com圖標

這裡的線程安全並不是說:由於 m 只能被初始化一次,所以只有初始化 m 的線程會阻塞,另外一個就立即跳過初始化過程返回了。

這裡的線程安全指的是:一個線程在初始化 m 的時候,其他線程執行到 m 的初始化這一行的時候,就會掛起。

示常式序中對 std::cout 的使用也不是線程安全的。


你這個例子不能說明它已經線程安全了。正確的驗證方法是

1、輸出現在的時間

2、啟動兩個線程調用create(而不是先調用一次之後啟動另一個線程)

3、「return m.value++;」改成輸出當前的時間

然後如果你發現三個時間一直都是x、x+5、x+5,那麼它就是線程安全的。


你自己過一下流程的Callstack:

create() -&> m.Construct() -&> value:=0 -&> cout("Create M") -&> wait(5) -&> return (m.Construct() ) -&>m.value++ -&> a= return ( create() ) -&> worker () -&> cout ("A) -&>join(worker)

|| -&> create() -&>m.value++ -&> b = return(create()) -&> cout("B")

注意你的worker啟動點是放在m.Constructor()之後的,並且是由之前的主線程的callstack保證的嚴格阻塞的,所以你的提問並不能證明你的m.Constructor是線程安全的,你只是證明了子線程的啟動和壓棧會消耗額外的時間,使得主線程的cout有更大的可能性跑到子線程的cout前面而已。


第二次執行create並沒有堵塞。


static local variable initialization 是絕對線程安全的,這一點由編譯器保證

Re: Applied: PATCH for thread-safe C++ static local initializationgcc.gnu.org

我試驗過你的代碼,構造函數確實只運行一次,也只有一次進入了阻塞態,並沒有出現你描述的行為:

static變數的構造函數只會執行一次,那麼兩次create時,肯定有一次沒有執行構造,如果是這樣的話, 後一個進入的create()時候, 為啥既沒有執行構造也變成阻塞態了

事實上,在C++中使用局部靜態變數初始化的線程安全性保證來構造單例模式是一個常用的方法。

C++ 線程安全的singleton如何實現blog.csdn.net


以上實驗環境:linux-4.13 gcc-5.4


static變數的初始化是在main執行之前的。你可以驗證一下。

所以static M m的確只會在main之前構造一次


我覺得這裡並沒有線程安全,沒有對m.val的並發讀寫的可能。雖然不知道c佳佳的happendsbefore怎麼規定的,但是創建線程一定發生在線程代碼執行之前這個關係我覺得一般都會這麼設計吧。

你提出的問題:

後一個進入的create()時候, 為啥既沒有執行構造也變成阻塞態了?

這個你怎麼看出來的…

看代碼我的理解是:

如果先主線程執行了一次create,裡面m初始化(阻塞五秒),然後開了子線程(注意裡面的語句還沒執行),然後列印A,主線程join掛起交出執行機會,然後子線程獲得執行的機會執行create,列印B,然後子線程退出,join返回,主線程退出。

上述流程是一個合法流程,似乎後一個create沒有什麼進入阻塞這個說法。

不過針對你的栗子,有個點我想說。除非什麼spec裡面寫了(對c艹不熟,不確定是不是沒有寫,但是我覺得應該沒有寫)我線程一定不是搶佔的,你不主動掛起我一定不會換個線程執行(如同協程那樣),你就不能說我B的列印不可能在A的列印的前面,也就是說未必每次都一定會先列印A,換個編譯器或者操作系統可能就不一樣了。而且我很好奇一點,如果B的列印真的發生在A前面,那個join調用沒問題嗎?


推薦閱讀:

TAG:C | 多線程編程 | C11 | 線程安全 |