歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux資訊 >> 更多Linux >> 來自 e-BIT 的珍品:雙 if 魔符

來自 e-BIT 的珍品:雙 if 魔符

日期:2017/2/27 14:17:34   编辑:更多Linux
  如果您發現您的代碼 99.99% 的時間在單 CPU 上運行,但是當您按比例增加到兩個或更多個 CPU 時,它很快就會崩潰,那麼這一珍品正適合您。 這一問題不僅與 Java 代碼在一個對稱多處理器(symmetric multiple processor,SMP)平台上運行的復雜程度有關,還與管道技術對“受保護的(protected)”代碼的影響有關。盡管您的代碼應該高效而且一致始終都很重要,但是由於 SMP 這樣的平台將誇大存儲模型中的一致性問題,因此,當您的應用程序在 SMP 平台上運行時尤其是如此。 雖然雙 if 子句是解決多線程應用程序所帶來的問題的一般方法,但它使您遇到“弱一致性”模型(CPU 管道技術、預測執行(speculative execution)等等)所帶來的問題(尤其是關於 SMP 系統)。因此,這個珍品簡而言之就是:如果您的應用程序將在 SMP 系統上運行,那麼請不要在您的代碼中使用雙 if 邏輯。現在,讓我們仔細看一下這個問題的原因和機理。 雙 if 邏輯 首先,請考慮一下把指針指向資源的下列代碼,其中 flag 表示有無該資源: IF flag == 0 { // no resource set flag acquire resource set pointer to resource } ELSE set pointer to resource 如果這段代碼是可重入的(即,可以由多個線程運行它),那麼它是非常危險的。請考慮一下如果一個線程在 if 子句的中途,此時另外一個線程發現 flag 已經被設置了,於是它就試圖去設置一個指針指向第一個線程還未分配的資源,結果會怎樣呢?幸運的是,這個問題很容易糾正,如下所示: IF flag == 0 { // no resource acquire resource ----- A set pointer to resource set flag } ELSE set pointer to resource 但是,現在我們又發現另外一個問題:一個線程阻塞在 A 點,另一個線程卻因發現 flag 被設為 0 而繼續執行。在這種情況下,這兩個線程都執行同一段代碼分配資源 — 這根本不是我們所希望的!然而,解決方法又是眾所周知:我們只要在執行獲取資源的代碼時,封鎖所有其它線程,如下所示: ENTER LOCK IF flag == 0 { // no resource acquire resource set pointer to resource set flag } ELSE set pointer to resource LEAVE LOCK (請注意我使用了 LOCK 這一術語來保持示例簡單。當然,在 Java 代碼中是用 synchronize 子句內的同步代碼來表示它)。 現在我們的代碼是線程安全的,但還不是高效的。獲取鎖(或 Java 術語中的管程(monitor))需要很多時間,而且鎖中的代碼比我們需要的要多。為了糾正這一點,我們把代碼改寫成下面這樣: IF flag == 0 { // no resource ----- A ENTER LOCK acquire resource set pointer to resource set flag LEAVE LOCK } ELSE set pointer to resource 這幾乎奏效了,但是我們又引入了前面的問題,一個線程在 A 點被中止,而另一個線程插入進來,因此造成了 CPU 的混亂。為了糾正這一點,我們使用著名的雙 if 子句: IF flag == 0 { // no resource ----- A ENTER LOCK IF flag == 0 acquire resource set pointer to resource set flag ELSE set pointer to resource LEAVE LOCK } ELSE set pointer to resource 通過添加雙 if 子句,我們已經盡了最大努力使代碼線程安全而且高效。許多高級編程技術方面的書中都推薦使用雙 if 邏輯來解決線程爭用。但是雙 if 邏輯在多個線程可以同時執行的 SMP 機器上並不安全。 您會說,可是某一時刻在“鎖住的”那部分代碼中只會有一個線程,那麼會出什麼問題呢?很多!這就是把這一技巧叫做“雙 if 魔符”的原因所在。


雙 if 魔符請考慮一下管道技術對上面這段代碼的影響: 由於 CPU 看不到任何依賴,所以可以以任何次序執行 if 子句裡的代碼。這會影響到下面用 * 標出的指令。 如果 flag 被設為 0,那麼與先計算“flag”再計算 flag 非 0 情況下的指針相比,計算指針(可能會是垃圾)並拋掉它可能是更高效的做法。所以,CPU 可以采用管道技術處理下面用 ** 標出的指令。 以下又是這些代碼,為說明上述觀點而加上了標記: **IF flag == 0 { // no resource ENTER LOCK IF flag == 0 *acquire resource *set pointer to resource *set flag ELSE set pointer to resource LEAVE LOCK } ELSE **set pointer to resource 盡管這似乎有點奇怪,但上面的代碼可能會象下面這樣執行: **set pointer to resource **IF flag == 0 { // no resource ENTER LOCK set pointer to resource IF flag == 0 *set flag *acquire resource ----- A *set pointer to resource ELSE LEAVE LOCK } ELSE 正如您所見到的,我們很敏感的代碼仍在鎖中,但是現在在分配資源之前設置 flag。因此,請想象一下線程 A 正在 A 點附近執行,此刻另外一個線程到來了。在 SMP 機器上,第二個線程可以在另一個 CPU 上與第一個線程同時執行;這個線程看到 flag 沒有被設置成 0(因為它在鎖的外部),它可以繼續把指針指向未分配資源 — 上當了! 此外,即使代碼按我們所寫的執行,我們仍然沒有免除災難,原因在於 CPU 可以采用管道技術處理以上用 ** 標出的代碼: 1. 線程 A 進入鎖住的那部分代碼。 2.線程 B 對第一個 if 語句求值並計算指針的值(在最後的 else 子句中)。 3.線程 B 得到“指針”(由於線程 A 還沒有指定它,所以是垃圾)的值。 4.在線程 A 完成並解鎖的同時,線程 B 將計算第一個 if 的值。 5.解鎖導致“flag”的值被清除(意味著線程 B 在它的高速緩存中具有的任何值都是無效的)。 6.線程 B 計算好第一個 if 子句的值,當然,現在它發現值為 true。 7.現在線程 B 使用前面那個是垃圾的指針值。 這就是 Java 雙 if 魔符。雖然其它的語言(比如 C/C++)讓您通過使用語言本機功能強制執行的次序來解決這個問題,Java 語言卻不行。為了安全的執行上述示例,您必須把所有代碼封裝在一個 synchronize 子句中,或者尋找另外一種寫法。



正如您所見到的,我們很敏感的代碼仍在鎖中,但是現在在分配資源之前設置 flag。因此,請想象一下線程 A 正在 A 點附近執行,此刻另外一個線程到來了。在 SMP 機器上,第二個線程可以在另一個 CPU 上與第一個線程同時執行;這個線程看到 flag 沒有被設置成 0(因為它在鎖的外部),它可以繼續把指針指向未分配資源 — 上當了! 此外,即使代碼按我們所寫的執行,我們仍然沒有免除災難,原因在於 CPU 可以采用管道技術處理以上用 ** 標出的代碼: 1. 線程 A 進入鎖住的那部分代碼。 2.線程 B 對第一個 if 語句求值並計算指針的值(在最後的 else 子句中)。 3.線程 B 得到“指針”(由於線程 A 還沒有指定它,所以是垃圾)的值。 4.在線程 A 完成並解鎖的同時,線程 B 將計算第一個 if 的值。 5.解鎖導致“flag”的值被清除(意味著線程 B 在它的高速緩存中具有的任何值都是無效的)。 6.線程 B 計算好第一個 if 子句的值,當然,現在它發現值為 true。 7.現在線程 B 使用前面那個是垃圾的指針值。 這就是 Java 雙 if 魔符。雖然其它的語言(比如 C/C++)讓您通過使用語言本機功能強制執行的次序來解決這個問題,Java 語言卻不行。為了安全的執行上述示例,您必須把所有代碼封裝在一個 synchronize 子句中,或者尋找另外一種寫法。



Copyright © Linux教程網 All Rights Reserved