[英]Data Races in JavaScript?
讓我們假設我運行這段代碼。
var score = 0;
for (var i = 0; i < arbitrary_length; i++) {
async_task(i, function() { score++; }); // increment callback function
}
從理論上講,我知道這會導致數據競爭,並且兩個線程試圖同時增加可能會導致單個增加,但是,nodejs(和 javascript)已知是單線程的。 我能保證分數的最終值將等於任意長度嗎?
我能保證分數的最終值將等於任意長度嗎?
是的,只要所有async_task()
調用只調用一次回調,就可以保證 score 的最終值等於任意長度。
Javascript 的單線程特性保證了永遠不會有兩個 Javascript 片段同時運行。 相反,由於瀏覽器和 node.js 中 Javascript 的事件驅動性質,一段 JS 運行完成,然后從事件隊列中提取下一個事件並觸發回調,該回調也將運行完成。
沒有中斷驅動的 Javascript 之類的東西(其中一些回調可能會中斷當前正在運行的其他一些 Javascript)。 一切都通過事件隊列進行序列化。 這是一個巨大的簡化,並防止了許多棘手的情況,否則當您有多個線程並發運行或中斷驅動代碼時,安全編程需要大量工作。
仍然存在一些需要關注的並發問題,但它們更多地與多個異步回調都可以訪問的共享狀態有關。 雖然在任何給定時間只有一個人會訪問它,但包含多個異步操作的一段代碼仍然有可能使某個狀態處於“中間”狀態,而它同時處於多個異步操作的中間一些其他異步操作可以運行並且可以嘗試訪問該數據的點。
您可以在此處閱讀有關 Javascript 事件驅動性質的更多信息: JavaScript 如何在后台處理 AJAX 響應? 該答案還包含許多其他參考資料。
另一個類似的答案討論了可能的共享數據競爭條件的類型: 此代碼是否會導致套接字 io 中的競爭條件?
其他一些參考:
如何防止事件處理程序在 javascript 中一次處理多個事件?
具有多個並發請求的 Node.js 服務器,它是如何工作的?
為了讓您了解 Javascript 中可能發生的並發問題(即使沒有線程和中斷,這是我自己的代碼中的一個示例。
我有一個 Raspberry Pi node.js 服務器,用於控制我家中的閣樓風扇。 它每 10 秒檢查兩個溫度探測器,一個在閣樓內,一個在屋外,並決定如何控制風扇(通過繼電器)。 它還記錄可以在圖表中顯示的溫度數據。 每小時一次,它將內存中收集的最新溫度數據保存到一些文件中,以便在斷電或服務器崩潰時持久保存。 該保存操作涉及一系列異步文件寫入。 這些異步寫入中的每一個都將控制權交還給系統,然后在調用異步回調信號完成時繼續。 因為這是一個低內存系統,並且數據可能會占用可用 RAM 的很大一部分,所以在寫入之前不會將數據復制到內存中(這根本不實用)。 所以,我正在將實時內存數據寫入磁盤。
在任何這些異步文件 I/O 操作期間的任何時候,在等待回調以表示所涉及的許多文件寫入完成時,服務器中的一個計時器可能會觸發,我會收集一組新的溫度數據和這將嘗試修改我正在編寫的內存數據集。 這是一個等待發生的並發問題。 如果它在我寫了一部分數據時更改了數據,並且在寫其余部分之前等待該寫完成,那么寫入的數據很容易最終損壞,因為我將寫出一部分數據,數據將從我的下方被修改,然后我將嘗試寫出更多數據而沒有意識到它已被更改。 那是並發問題。
我實際上有一個console.log()
語句,它在我的服務器上發生此並發問題時明確記錄(並且由我的代碼安全處理)。 它在我的服務器上每隔幾天發生一次。 我知道它就在那里,而且是真的。
有很多方法可以解決這些類型的並發問題。 最簡單的方法是在內存中復制所有數據,然后寫出副本。 因為沒有線程或中斷,所以在內存中進行復制可以避免並發(在復制中間不會屈服於異步操作以創建並發問題)。 但是,在這種情況下這是不切實際的。 所以,我實現了一個隊列。 每當我開始寫作時,我都會在管理數據的對象上設置一個標志。 然后,只要系統想要在設置該標志時添加或修改存儲數據中的數據,這些更改就會進入隊列。 設置該標志時不會觸及實際數據。 當數據已安全寫入磁盤時,該標志將被重置並處理排隊的項目。 安全地避免了任何並發問題。
因此,這是您必須關注的並發問題的一個示例。 Javascript 的一個很好的簡化假設是,一段 Javascript 將運行到完成,而不會有任何線程被中斷,只要它不故意將控制權返回給系統。 這使得處理上述並發問題變得更加容易,因為除非您有意識地將控制權交還給系統,否則您的代碼永遠不會被中斷。 這就是為什么我們在我們自己的 Javascript 中不需要互斥體和信號量以及其他類似的東西。 如果需要,我們可以像上面描述的那樣使用簡單的標志(只是一個常規的 Javascript 變量)。
在任何完全同步的 Javascript 中,您永遠不會被其他 Javascript 打斷。 在處理事件隊列中的下一個事件之前,一段同步的 Javascript 將運行完成。 這就是 Javascript 作為“事件驅動”語言的含義。 舉個例子,如果你有這個代碼:
console.log("A");
// schedule timer for 500 ms from now
setTimeout(function() {
console.log("B");
}, 500);
console.log("C");
// spin for 1000ms
var start = Date.now();
while(Data.now() - start < 1000) {}
console.log("D");
您將在控制台中獲得以下信息:
A
C
D
B
在當前的 Javascript 部分運行完成之前無法處理計時器事件,即使它很可能比這更早添加到事件隊列中。 JS 解釋器的工作方式是它運行當前的 JS,直到將控制權返回給系統,然后(並且僅在那時),它從事件隊列中獲取下一個事件並調用與該事件關聯的回調。
這是幕后事件的順序。
console.log("A")
是輸出。console.log("C")
是輸出。console.log("D")
是輸出。console.log("B")
。setTimeout()
回調完成執行並且解釋器再次檢查事件隊列以查看是否有任何其他准備好運行的事件。 Node 使用事件循環。 您可以將其視為隊列。 所以我們可以假設,你的 for 循環放置了function() { score++; }
function() { score++; }
回調arbitrary_length
在此排隊時間。 之后js引擎將這些一一運行,每次都增加score
。 所以是的。 如果沒有調用回調或從其他地方訪問score
變量,這是唯一的例外。
實際上,您可以使用此模式並行執行任務,收集結果並在每個任務完成時調用單個回調。
var results = [];
for (var i = 0; i < arbitrary_length; i++) {
async_task(i, function(result) {
results.push(result);
if (results.length == arbitrary_length)
tasksDone(results);
});
}
函數的兩個調用不能同時發生(b/c 節點是單線程的),所以不會有問題。 唯一的問題是如果在某些情況下 async_task(..) 丟棄回調。 但是,如果,例如,'async_task(..)' 只是用給定的函數調用 setTimeout(..),那么是的,每個調用都會執行,它們永遠不會相互沖突,並且 'score' 將具有預期的值, 'arbitrary_length', 最后。
當然,'arbitrary_length' 不能大到耗盡內存或溢出任何持有這些回調的集合。 但是沒有線程問題。
我確實認為對於其他人來說值得注意的是,您的代碼中有一個常見的錯誤。 對於變量 i,在將其傳遞到 async_task() 之前,您需要使用 let 或重新分配給另一個變量。 當前的實現將導致每個函數獲得 i 的最后一個值。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.