當前位置:
首頁 > 新聞 > 避免使用AtomArrayBuffers中的競爭條件

避免使用AtomArrayBuffers中的競爭條件

在《ArrayBuffers和SharedArrayBuffers的介紹》一文中,我談到了在使用SharedArrayBuffers時是如何可能導致競爭條件的,這使得大家很難使用SharedArrayBuffers。另外在文章的最後我還提到不希望應用程序開發人員直接使用SharedArrayBuffers。

但是,具有其他語言的多線程編程經驗的庫開發人員可以使用這些新的低級API來創建更高級別的工具。如果是這樣,那麼應用程序開發人員則可以直接使用這些工具而不用接觸SharedArrayBuffers或Atomics。

即使你可能用不到SharedArrayBuffers或Atomics,但我認為了解它們是如何工作的,仍然會有助於你日後的程序開發。所以在本文中,我將介紹並發可以帶來什麼樣的競爭條件,以及Atomics如何幫助庫避免它們。

但首先,需要知道什麼是競爭條件?

競爭條件指多個線程或者進程在讀寫一個共享數據時結果依賴於它們執行的相對時間的情形,競爭條件發生在當多個進程或者線程在讀寫數據時,其最終的結果依賴於多個進程的指令執行順序。假設兩個進程P1和P2共享了變數a。在某一執行時刻,P1更新a為1,在另一時刻,P2更新a為2。因此兩個任務競爭地寫變數a。在這個例子中,競爭的「失敗者」(最後更新的進程)決定了變數a的最終值。多個進程並發訪問和操作同一數據且執行結果與訪問的特定順序有關,稱為競爭條件。

簡單的說,就是當一個變數在兩個線程之間共享時,就很可能發生一個非常簡單的競爭情境。假設一個線程想載入一個文件,另一個線程則要檢查它是否存在。他們共享一個變數file_exists函數來檢查文件或目錄是否存在,以方便進行通信。

最初,fileExists設置為false。

只要線程2中的代碼首先運行,文件將被載入。

但如果線程1中的代碼首先運行,那麼它將向用戶記錄一個錯誤,表示該文件不存在。

但這不是問題的所在,不是文件不存在,真正的問題是競爭條件。

許多JavaScript開發人員經常會遇到這種競爭條件,即使是在單線程代碼的情境之下。你不必理解有關多線程的任何內容,只需看看為什麼會發生競爭條件。

但是,有些類型的競爭條件在單線程代碼中是不可能發生的,但是當你使用多個線程進行編程並且這些線程共享內存時,可能會發生這種情況。

Atomics是如何處理不同類別的競爭條件的?

讓我來介紹一些不同類型的競爭條件以及你如何多線程代碼中使用Atomics防止競爭條件,不過,這不包括所有可能的競爭條件,但通過我的介紹,你應該給能對API的做法有所了解。

在開始之前,我想再說一遍,你不應該直接使用Atomics。編寫多線程代碼是一個眾所周知的難題。所以,你應該使用可靠的庫來使用多線程代碼中的共享內存。

單一線程操作中的競爭條件

假設你有兩個線程增加相同的變數,無論哪個線程首先運行,你可能都會認為最終的結果將是一樣的。

但即使在源代碼中,增加一個變數看起來就像一個操作,當你查看編譯的代碼時,它其實不是一個單一的操作。

在CPU級別,增加一個值需要三條指令,這是因為計算機具有長期記憶和短期記憶。

所有線程共享長期記憶,但基於寄存器的短期記憶是不會在線程之間共享的。

每個線程都需要將內存中的值從其內存中取出來,之後,可以在短期記憶中對該值進行計算。然後它將這個值從短期記憶回溯到長期記憶。

如果線程1中的所有操作首先發生,然後線程2中的所有操作都會發生,你將最終得到想要的結果。

但是如果它們在時間上是交錯的,則線程2已經被拉入其寄存器的值與內存中的值不同步。這意味著線程2不考慮線程1的計算值,也就是說,它只是消除了線程1,用自己的值寫入內存的值。

Atomics操作所做的一件事是將人們認為是單一操作而計算機視為多個操作的這些操作,讓計算機統一將它們視為單個操作。這個操作之所以被稱為Atomics操作的原因是因為他們採取的操作通常會有多個指令,指令可以暫停和恢復,並且可以使它們全部發生在瞬間,就像是一個指令一樣,這就像一個不可分割的Atomics。

使用Atomics操作,增量代碼看起來有點不同。

現在我們使用的是Atomics.add,遞增變數所涉及的不同步驟不會在線程之間混合。相反,會按著順序,當一個線程進行其Atomics操作時,會阻止另一個線程啟動。然後等操作完成後,另一個將開始自己的Atomics操作。

避免競爭條件發生的Atomics方法有:

·Atomics.add

·Atomics.sub

·Atomics.and

·Atomics.or

·Atomics.xor

·Atomics.exchange

你會注意到這個列表是相當有限的,它甚至不包括分割和倍增的辦法等。不過,庫開發人員可以為其他情境創建類似Atomics的操作。

為此,開發人員將使用Atomics.compareExchange。這樣,你可以從SharedArrayBuffer獲取值,對其執行操作,並且只有在你首次檢查後,確認沒有其他線程已更新的情況下才將其寫回SharedArrayBuffer。如果另一個線程已更新,那麼你可以獲得新值,然後重試。

多個線程操作中的競爭條件

通過上面的介紹,你已經了解了這些Atomics操作有助於在單次操作期間避免競爭條件。但有時你想要更改對象上的多個值即使用多個操作,並確保沒有其他人同時對該對象進行更改。基本上,這意味著在對對象的每次更改通過期間,該對象都處於鎖定狀態,而其他線程無法訪問。

Atomics對象雖然不提供任何工具來直接處理,但它確實提供了庫作者可以用來處理這個問題的工具。庫作者可以創建一個鎖。

如果代碼想要使用鎖定的數據,它必須對該數據進行解鎖。同樣它也可以使用鎖來鎖定其他線程。只有在解鎖成功時,才能訪問或更新數據。

為了構建一個鎖,庫作者將使用Atomics.wait和Atomics.wake,加上其他的工具,如Atomics.compareExchange和Atomics.store。如果你想看看它們是如何工作的,看看這個。

在這種情況下,線程2將獲取數據鎖,並將鎖定值設置為true。這意味著線程1無法訪問數據,直到線程2解鎖。

如果線程1需要訪問數據,它將嘗試獲取鎖。但是由於鎖已經在使用,所以不能重複使用。這時線程就會處於等待狀態,所以它將被阻止,直到被解鎖。

一旦線程2完成,它將調用解鎖,該鎖將通知一個或多個等待線程現在可用。

這樣使用鎖的線程就會鎖定數據供自己獨自使用:

鎖庫將使用Atomics對象上的許多不同方法,其中最重要的方法是:

·Atomics.wait;

·Atomics.wake;

由指令重新排序引起的競爭條件

經過測試,Atomics可以同時處理三個同步問題,這確實非常令人驚訝。

你可能沒有意識到這一點,但是你寫的代碼並不是按照你期望的順序來運行。編譯器和CPU都會重新排序你編寫的代碼,使其運行速度更快。

例如,假設你已經編寫了一些計算總和的代碼,你想在計算結束時設置一個標誌。

為了編譯這個代碼,你需要決定每個變數使用哪個寄存器。之後,你可以將源代碼轉換為該設備的說明。

到目前為止,一切都如預期。

如果你不了解計算機在晶元級別的工作原理以及它們用於執行代碼工作的流水線,那你的代碼中的第2行需要稍等一下才能執行。

大多數計算機將運行指令的過程分解成多個步驟,這樣可以確保CPU的所有不同部分始終處於運行狀態,這樣才能充分利用CPU的性能。

以下是我在處理一個實際案例時的步驟:

1.從內存中讀取下一條指令;

2.找出告訴我要做什麼的指令,也就是解碼指令,並從寄存器獲取該值;

3.執行指令;

4.將結果寫回寄存器;

這是一條指令如何通過管道的步驟,理想情況下,我希望直接遵循第二條指令,即一旦進入第二階段,我們就能要獲取下一條指令。

問題是指令#1和指令#2之間存在依賴關係。

你可以暫停CPU,直到指令#1更新了寄存器中的subTotal,但這會減慢運行速度。

為了使運行更有效率,很多編譯器和CPU將做的是重新排序代碼,它們將尋找不使用subTotal或total的其他指令,並將它們移動到兩行之間。

這麼做就保持了穩定的指令流通過管道,因為第3行不依賴於第1行或第2行中的任何值,所以編譯器或CPU表明可以像這樣重新排序。當你運行在單個線程中時,不管發生什麼,在整個函數完成之前,其他代碼甚至不會看到這些值。

但是當另一個線程在另一個處理器上同時運行時,情況並非如此。另一個線程不需要等到函數完成才能看到這些更改。一旦它們被記錄回來,它就可以看到這些值,所以該情況可以說是一個被設置在總和之前的isDone運行。

如果你使用isDone來作為計算總和的方法並準備在其他線程中使用,則這種重新排序將創建競爭條件。

Atomics試圖解決一些這些錯誤,當你使用Atomic寫入時,就像將代碼放在兩個部分之間。

Atomics操作相對於彼此不重新排序,其他操作也不會在其周圍移動。特別是,經常用於強制排序的兩個操作是:

·Atomics.load;

·Atomics.store;

在Atomics.store完成將其值並重寫回內存之前,函數源代碼中的Atomics.store之上的所有變數更新都將得到保證。即使非Atomics指令相對於彼此重新排序,它們都不會被移動到源代碼下面的Atomics.store。

在保證Atomics.load獲取其值後,在函數中的Atomics.load之後的所有值都將變為可變負載。之後,即使非Atomics指令被重新排序,它們也不會被移動到源代碼之前的Atomics.load。

注意:這裡顯示的while循環稱為自旋鎖,效率非常低,自旋鎖是專為防止多處理器並發而引入的一種鎖,它在內核中大量應用於中斷處理等部分。如果它在主線程上,它可以使你的應用程序停止,幾乎可以肯定你不想在實踐中的編碼中使用它。

另外,這些方法並不意味著直接用在應用程序代碼中。相反,庫將使用它們來創建鎖。

總結

編程共享內存的多個線程很難,有很多不同種類的競爭條件會阻礙你。

這就是為什麼你不想直接在應用程序代碼中使用SharedArrayBuffers和Atomics。所以,你應該依賴具有多線程經驗的開發人員的經過驗證的庫,以及經過實踐驗證的內存模型。

由於SharedArrayBuffer和Atomics還處於早期研發階段,所以有很多庫還尚未創建。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 嘶吼RoarTalk 的精彩文章:

Glibc堆漏洞利用基礎-深入理解ptmalloc2 part1
通過Edge瀏覽器遠程代碼執行PoC發布

TAG:嘶吼RoarTalk |