併發、可重入性與信號安全

併發、可重入性與信號安全

這篇博客主要記錄的是關於可重入性的相關定義,以及關於併發安全的思考。node

可重入性

在不一樣語言中,因爲語言標準以及運行期環境規定的不一樣,可重入性的具體定義可能有所不一樣。這裏聊的是C++語言中的可重入性。程序員

所謂可重入性(reetrant),指的是同時具有併發安全中斷安全的特徵,這是目前爲止我對可重入性的認識,也是這篇博客在寫下時給可重入性下的定義。編程

這個認知可能並不許確,由於在wiki上的定義是這樣的。設計模式

若一個程序或子程序能夠「在任意時刻被中斷而後操做系統調度執行另一段代碼,這段代碼又調用了該子程序不會出錯」,則稱其爲可重入(reentrant或re-entrant)的。即當該子程序正在運行時,執行線程能夠再次進入並執行它,仍然得到符合設計時預期的結果。與多線程併發執行的線程安全不一樣,可重入強調對單個線程執行時從新進入同一個子程序仍然是安全的。安全

可是在不少中文博客裏,聊到可重入性的時候每每也會把併發安全混爲一談。實際上來講的話......一個可重入的函數,經常也是併發安全的。多線程

那麼先從併發安全講起吧。併發

併發安全性和可重入性

所謂併發安全已是老生常談了。編程語言

以一段很是簡單的代碼爲例,咱們打算初始化一個對象,這個對象被兩個線程共享。ide

void initialize(Something** someshit) {
  if(!*someshit) {
    *someshit = createSomeShit();
  }
}

顯而易見,若是線程在執行到特定環節時發生了切換函數

void initialize(Something** someshit) {
  if(!*someshit) {
    // <-------- 線程切換
    // 線程2() {
    // initialize(something);
    // }
    // 線程切換 --------->
    *someshit = createSomeShit();
  }
}

那麼 createSomeShit這段代碼就會被執行兩次。

顯然這和咱們預期的行爲不符。

這裏要聊的不是併發,而是......可重入性。因此咱們再看看這個函數可否被重入。

按照 wiki 提供的定義,函數可重入指的是

在任意時刻被中斷而後操做系統調度執行另一段代碼,這段代碼又調用了該子程序不會出錯。

符合嗎?不。爲何?由於一樣在那個線程切換的位置上中斷,而後再另外一段代碼裏再次執行這個函數,也會觸發一樣的問題,致使createSomeShit被執行兩次。

void initialize(Something** someshit) {
  if(!*someshit) {
    // <-------- 被中斷
    // 中斷處理函數() {
    //   initialize(something);
    // }
    // 中斷結束 --------
    *someshit = createSomeShit();
  }
}

能夠看出,那些線程不安全的代碼,都是不可重入的。

那麼,線程安全的代碼,就必定是可重入的嗎?

中斷安全性,或者叫信號安全性

中斷這個東西對其餘編程語言的用戶來講可能會少見一些,在C/C++語言裏,中斷並非什麼新鮮話題。

在C標準庫中,規定了一系列的信號和信號處理方法。關於信號的定義能夠參考這個

當進程接收到信號的時候,當前正在執行的代碼就會被中斷——注意了,這回,鎖救不了你。

在C/C++中,中斷處理是由一個函數進行。在函數裏可能會調用到中斷時正在執行的函數。那麼問題來了——一個線程安全的函數,是中斷安全的函數嗎?

void initialize(Something** someshit, std::mutex& realshit) {
  std::lock_guard<std::mutex>(realshit);
  if(!*someshit) {
    *someshit = createSomeShit();
  }
}

看上去歲月靜好~一切線程切換的問題,都被那句std::lock_guard<std::mutex>(realshit)給擋在了牆的另外一邊。

可是......

void initialize(Something** someshit, std::mutex& realshit) {
  std::lock_guard<std::mutex>(realshit);
  if(!*someshit) {
    // <----- 調皮的用戶按下了 Ctrl-C
    // 中斷處理函數() {
    //   initialize(someshit, realshit);
    //   // inside initialize {
    //   //   std::lock_guard<std::mutex>(realshit); // DEAD LOCK
    //   // }
    // }
    *someshit = createSomeShit();
  }
}

看這裏~

std::lock_guard<std::mutex>(realshit);
// 進入信號處理
std::lock_guard<std::mutex>(realshit);

好了,GG。死鎖在這個時候發生了。

經驗豐富的大佬可能注意到了,咱還能夠用std::recursive_mutex啊!

這裏就要提到一個很遺憾的問題了:C/C++的語言標準給了哪些保證。

C對信號處理函數的定義很粗暴,除了abort_Exitquick_exitsignalstdatomic.h的免鎖原子函數atomic_is_lock_free與任何類型的原子參數這些函數之外,任何標準庫函數的調用,行爲都是未定義的。

C++對信號處理函數的定義則更加複雜,限制比之C更加嚴格。畢竟標準庫要龐大得多......也不是不能理解。

標準中有個一個地方的描述很微妙:......免鎖的

換言之,誰又保證了信號處理函數必然和你但願的那個線程是同一個線程呢?

std::recursive_mutex的實現依賴於平臺提供的系統API,反正我沒有找到語言標準中相關的規定要求信號處理函數必須和main函數在同一個線程,因此我認爲這是平臺相關的問題:這樣的代碼是不可移植的

按照設計模式原則,咱們是面向接口——也就是標準文檔編程,而不是面對實現——Visual C++、GCC、MinGW或者哪一個中東土豪在將來某天突發奇想送我一臺MIPS的超算的話。

到業務層面的話會更靈活一些——反正我只在某環境下跑,等公司何時全面換平臺了,咱再能改則改,改不了就跑路。

遞歸函數和可重入

遞歸和重入有必定的類似性,但又有所不一樣。

一個遞歸函數,直覺上來說,好像應該是可重入的:由於它要調用本身。

那麼......事實上呢?

寫個比較騷的遞歸刪除鏈表節點的例子。

void removeNode(Node* node, int length) {
  if(length > 0) {
    Node* tmp = node.prev;
    node.next.prev = tmp;
    // <------ 出現了!中斷獸!
    // 不用看了,Node之間的聯結已經被破壞了
    // 離開了!中斷獸!-------->
    tmp.next = node.next;
    freeNode(node);
    removeNode(tmp.next, length-1);
  }
}

輕易地否認了遞歸函數=可重入函數的直覺想法。

深究下去,又到了線程安全——而後是死鎖——而後提出了std::recursive_mutex或者其餘相似的操做——最後走到平臺相關的API和保證——失去可移植性。

爲何我一直在提可移植性?

emmmm,大概是裝逼如風,常伴吾身吧。

標準庫好煩人啊

C/C++語言的標準庫是出了名的——但不是好的方面,而是他們總在修修補補又一年。

C標準庫還好說——畢竟語言自己沒啥特性,全靠各類平臺提供API撐着。標準庫改來改去也只是割個雙眼皮的程度。

C++要更騷氣一些,每隔幾年就整個容,簡直不給人活路。

就中斷安全來講,雖然不知道內部怎麼實現的,可是......printf這樣的函數在信號處理函數裏調用的話,也算是未定義行爲。

認輸吧,你是鬥不過標準的。該依賴平臺行爲的時候,就去依賴平臺行爲吧。

文檔引用

懶得找原文,直接看cppreference對signal的說法就好。有興趣的話能夠找又臭又長的WG14 - N1570 - C11,還有WG21 - N4659 - C++17這兩本標準文檔。

尾聲

因而這會兒就到了其餘各類語言的用戶慣例吐槽的時候:

...大佬是公司裏惟一用C++寫代碼的人。他對人說話,老是滿口「目標平臺」、「標準」、「可移植性」之類的話,叫人半懂不懂的。由於他老是說「C++天下第一!」,別人便從他說的那些半懂不懂的話裏,替他取下個綽號,叫C++大神。

C++大神一到公司裏,程序員們便看着他笑,有的叫道:「C++大神,你的代碼又編譯出錯了!」

他不回答,對前臺說:「倒上特濃的咖啡,今天也要加班到夜裏。」便拿出員工卡。程序員們又高聲叫嚷道:「你必定又用上新標準了吧?」

C++大神睜大眼睛說,「你怎麼憑空污人清白!」

「什麼清白?我前天親眼看見你的代碼編譯報了錯,整整十幾MB的日誌!」

C++大神便漲紅了臉,額上的青筋條條綻出,爭辯道,「編譯器報錯怎麼能叫錯......C++......編譯器不支持,那能算錯麼?」

接連即是難懂的話,什麼「CONCEPT還不加入標準」、「未定義行爲就該是編譯錯誤」、「SFINAE就是給編譯器開洞」、「boost大法好,天滅std::experimental」,引得衆人都鬨笑起來:店內外充滿了快活的空氣。

相關文章
相關標籤/搜索