摘要:如果期間有其它線程更新了,則會(huì)先拿到新的值重新運(yùn)算一次多運(yùn)算的競爭條件這些運(yùn)算符成功避免了單運(yùn)算中的競爭條件。
這是圖解 SharedArrayBuffers 系列的第三篇:
內(nèi)存管理碰撞課程
圖解 ArrayBuffers 和 SharedArrayBuffers
用 Atomics 避免 SharedArrayBuffers 競爭條件
譯者注:文中會(huì)多次出現(xiàn)“線程(threads)”,這個(gè)翻譯其實(shí)并不準(zhǔn)確,但不會(huì)妨礙理解
上篇文章我介紹了什么情況下使用 SharedArrayBuffers 會(huì)導(dǎo)致競爭條件,這讓使用 SharedArrayBuffers 變得很困難,我們并不希望應(yīng)用開發(fā)者直接就這么使用 SharedArrayBuffers
但是在多線程編程方面經(jīng)驗(yàn)豐富的庫開發(fā)者可以使用這些底層 API 創(chuàng)造出高級(jí)的工具,應(yīng)用開發(fā)者可以直接使用這些工具而不用去直接接觸 SharedArrayBuffers 和 Atomics
即使你工作中不需要直接接觸 SharedArrayBuffers 和 Atomics,我覺得去理解它的工作原理也是很有意思的。因此,在這篇文章里我會(huì)解釋下哪些競爭條件會(huì)產(chǎn)生,以及 Atomics 是如何解決這些問題的
但是,首先,什么是競爭條件呢?
競爭條件:之前看過的例子如果有兩個(gè)線程使用同一個(gè)變量,那么就有可能產(chǎn)生競爭條件,這是最簡單的情況。再具體點(diǎn),假設(shè)一個(gè)線程要加載一個(gè)文件,而另一個(gè)線程要檢查這個(gè)文件是否存在(譯者注:這里應(yīng)該是檢查并設(shè)置存在標(biāo)志位),它們會(huì)使用到同一個(gè)變量 fileExists 去通信
初始的時(shí)候,fileExists 被設(shè)置為 false
一旦線程 2 先運(yùn)行,文件就會(huì)被加載
但是如果線程 1 先運(yùn)行,就會(huì)向用戶拋一個(gè)錯(cuò)誤,說文件不存在
但是這不是問題的關(guān)鍵,文件存在與否問題不大,真正的問題在于競爭條件
即使在單線程代碼里,許多 JavaScript 開發(fā)者也會(huì)遇到這類競爭條件,你不需要理解多線程就能搞明白為什么會(huì)競爭
然而,有些競爭條件在單線程里就沒法發(fā)生,只可能在有內(nèi)存共享的多線程里發(fā)生
不同類型的競爭條件以及 Atomics 是如何解決的現(xiàn)在說點(diǎn)多線程里不同類型的競爭條件,看看如何用 Atomics 解決的。這個(gè)并沒有覆蓋所有情況,但是卻會(huì)給你提供一些思路去理解為什么 Atomics 的 API 會(huì)提供這些方法
開始之前,需要再次重申:你不應(yīng)該直接使用 Atomics!寫多線的代碼本來就是個(gè)很苦逼的事情,你應(yīng)該直接使用可靠的庫去處理多線程中共享內(nèi)存問題
單個(gè)運(yùn)算的競爭條件假設(shè)有兩個(gè)線程同時(shí)增加某個(gè)變量的值,你可能認(rèn)為,無論哪個(gè)線程先運(yùn)行,最終的結(jié)果是一樣的
在代碼里,即使增加一個(gè)變量這種操作看起來像是一個(gè)操作,但如果看到編譯后的代碼,會(huì)發(fā)現(xiàn)并不是
從 CPU 層面看,增加一個(gè)變量值需要三條指令,這是因?yàn)橛?jì)算機(jī)同時(shí)有長期存儲(chǔ)器和短期存儲(chǔ)器(這個(gè)在其它文章里會(huì)說)
所有的線程共享同一個(gè)長期存儲(chǔ)器(內(nèi)存),但是短期存儲(chǔ)器(寄存器)并不是共享的
每個(gè)線程需要把值先從內(nèi)存搬到寄存器,之后就可以在寄存器上進(jìn)行計(jì)算了,再然后會(huì)把計(jì)算后的值寫回內(nèi)存
如果線程 1 的所有的操作都先執(zhí)行,之后執(zhí)行所有線程 2 的操作,最終會(huì)得到我們的預(yù)期的結(jié)果
但是,如果它們間隔著執(zhí)行,從線程 2 的里移到寄存器的值就無法與內(nèi)存的值同步了,這意味著線程 2 會(huì)無法用到線程 1 的計(jì)算結(jié)果。相反,它線程 2 會(huì)用覆蓋掉線程 1 寫回內(nèi)存的值
原子操作做的一件事就是在多線程中讓計(jì)算機(jī)按照人所想的單操作方式工作
這就是為什么被叫做原子操作,因?yàn)樗梢宰屢粋€(gè)包含多條指令(指令可以暫停和恢復(fù))的操作執(zhí)行起來像是一下子就完了,就好像一條指令,類似一個(gè)不可分割的原子
使用原子操作會(huì)讓加法變得有點(diǎn)不一樣
現(xiàn)在,我們可以使用 Atomics.add 了,加法執(zhí)行過程中不會(huì)因?yàn)槎嗑€程而被打亂。一個(gè)線程在執(zhí)行完原子操作前會(huì)阻止其它線程執(zhí)行,之后其它線程才會(huì)執(zhí)行自己的原子操作
Atomics 中幫助避免競爭的方法有:
Atomics.add
Atomics.sub
Atomics.and
Atomics.or
Atomics.xor
Atomics.exchange
你會(huì)發(fā)現(xiàn)這個(gè)列表數(shù)量很有限,甚至沒有除法和乘法。不過,庫的開發(fā)者會(huì)提供類似這些常見原子操作的
庫的開發(fā)者會(huì)借助 Atomics.compareExchange 從 SharedArrayBuffer 拿到值,應(yīng)用相應(yīng)的操作,然后只有在自上次檢查到現(xiàn)在沒有其它線程更新的情況下才會(huì)去寫回。如果期間有其它線程更新了,則會(huì)先拿到新的值重新運(yùn)算一次
多運(yùn)算的競爭條件這些 Atomic 運(yùn)算符成功避免了“單運(yùn)算”中的競爭條件。但是,有時(shí)你會(huì)同時(shí)改變一個(gè)對象上的多個(gè)值(使用多個(gè)運(yùn)算),在此期間,你并不希望有其它的任務(wù)也在修改這個(gè)對象。簡單說,就是在你修改這個(gè)對象期間,這個(gè)對象是處于禁閉狀態(tài),其它線程不可以訪問
Atomics 沒有提供任何方法去做這個(gè)事,但是卻為庫開發(fā)者提供了相應(yīng)的方案,庫開發(fā)者可以通過鎖來達(dá)到目的
如果代碼想使用某個(gè)被鎖住的數(shù)據(jù),首先它需要去請求鎖,之后它會(huì)用這個(gè)鎖把其它線程鎖在外面,只有它可以訪問和更新這塊數(shù)據(jù)
庫開發(fā)者會(huì)通過使用 Atomics.wait 和 Atomics.wake,以及可選的 Atomics.compareExchange 和 Atomics.store 創(chuàng)建一個(gè)鎖。想了解更多可以看下這篇文章 簡單鎖的實(shí)現(xiàn)
這種情況下,線程 2 會(huì)請求到鎖,并把值設(shè)置為 true,這意味著直到線程 2 交出鎖前,線程 1 是無法訪問的
如果線程 1 想要訪問這塊數(shù)據(jù),它會(huì)試圖請求鎖。但是因?yàn)殒i處于被使用狀態(tài),它無法拿到,它于是只能出于等待狀態(tài)直到鎖可用
一旦線程 2 結(jié)束了,它會(huì)調(diào)用 unlock,鎖會(huì)通知其它等待的線程自己空出來啦
那個(gè)線程就會(huì)拿起鎖,鎖住數(shù)據(jù)供自己使用
實(shí)現(xiàn)一個(gè)鎖可能需要依賴很多 Atomics 的方法,但是用的最多的是下面兩個(gè):
Atomics.wait
Atomics.wake
指令重排導(dǎo)致的競爭條件這里還有第三種同步問題需要用 Atomics 處理,這類問題可能會(huì)很神奇
你可能感覺不到,你寫的代碼很可能根本沒按你期望的順序執(zhí)行,因?yàn)榫幾g器和 CPU 會(huì)嘗試重排指令使得代碼更快地運(yùn)行
比如,你寫了一些代碼去計(jì)算總和,你想的是計(jì)算完了要設(shè)置一個(gè)標(biāo)記
編譯的時(shí)候需要決定每個(gè)變量該用哪個(gè)寄存器,之后就可以把代碼翻譯成機(jī)器的指令了
目前為止,一切都在掌握中
如果你對計(jì)算機(jī)芯片級(jí)的原理不理解的話,可能你沒發(fā)現(xiàn)到第 2 行需要等待下才能執(zhí)行
大多數(shù)的計(jì)算機(jī)會(huì)把一個(gè)指令拆分為多個(gè)步驟,這使得 CPU 可以被充分利用
下面是一個(gè)指令執(zhí)行步驟的例子:
從內(nèi)存里拿到下一個(gè)指令
指令解碼,從寄存器拿值
執(zhí)行指令
結(jié)果寫回寄存器
這就是指令如何像流水線工人一樣工作,理想的情況是第二個(gè)指令會(huì)緊緊地跟著第一個(gè)指令,當(dāng)?shù)谝粋€(gè)指令進(jìn)行到步驟 2 的時(shí)候,第二個(gè)指令進(jìn)行步驟 1
問題是,指令 1 和指令 2 存在依賴
CPU 需要一直等待直到指令 1 更新了寄存器里的 subTotal,但是這就使執(zhí)行變慢了
為了讓這一切更加高效,很多編譯器和 CPU 會(huì)記錄好代碼,找到不依賴 subTotal 或 total 的指令,然后移到兩個(gè)指令之間
這會(huì)讓指令執(zhí)行保持著一個(gè)很穩(wěn)定的流水線
因?yàn)榈谌胁灰蕾嚾魏吻皟尚械闹担幾g器和 CPU 認(rèn)為它是安全的。在單線程里運(yùn)行時(shí),直到運(yùn)行完不會(huì)有其它代碼看到這些
但是當(dāng)有另一個(gè) CPU 上的線程也在同時(shí)運(yùn)行,情況就不妙了。其它線程不需要一直等到函數(shù)執(zhí)行完畢,只要值寫到內(nèi)存里它就可以看到,因此,它會(huì)認(rèn)為 isDone 是在 total 前設(shè)置的
如果你用 isDone 作為 total 被計(jì)算好用于其它線程的標(biāo)記,這里就會(huì)產(chǎn)生競爭條件
Atomics 試圖去解決這些問題,使用 Atomic 的時(shí)候就像在代碼塊上加了個(gè)圍欄
Atomic 操作相互之間不會(huì)重排,其它操作也不會(huì)移動(dòng)到它們的周圍。其中,有兩個(gè)經(jīng)常用到的操作:
Atomics.load
Atomics.store
Atomics.store 之前的代碼可以保證在 Atomics.store 之前運(yùn)行完并把值寫回內(nèi)存。即使非原子指令相互之間重排了,也不會(huì)移到 Atomics.store 的下面
所有 Atomics.load 后面的變量可以保證只會(huì)在 Atomics.load 后面取得值。即使非原子指令重排了,也不會(huì)有指令會(huì)移到 Atomics.load 上面
提示:這里我寫的一個(gè) while 循環(huán)使用了自旋鎖,很低效。如果它在主線程上運(yùn)行的話,會(huì)讓你的應(yīng)用程序有無響應(yīng)一段時(shí)間,你不應(yīng)該在實(shí)際代碼里用
再次提醒,這些方法不建議直接在應(yīng)用程序里使用,庫開發(fā)者會(huì)用這些創(chuàng)造鎖供使用
總結(jié)有內(nèi)存共享的多線程編程是很蛋疼的,有太多競爭條件的陷進(jìn)等著你往里跳
這就是為什么你不會(huì)喜歡直接在應(yīng)用程序里使用 SharedArrayBuffers 和 Atomics。相反,你應(yīng)該使用一個(gè)由多線程方面經(jīng)驗(yàn)豐富的開發(fā)者開發(fā)的可靠的庫,他肯定會(huì)內(nèi)存模型研究很透徹
SharedArrayBuffer 和 Atomics 才出來沒多久,這樣的庫還沒有呢,但是新的 API 已經(jīng)足夠去構(gòu)建這些
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/83577.html
摘要:因?yàn)橛辛祟愋筒聹y,引擎通常會(huì)比真實(shí)需要預(yù)留更多的空間。如果你是手動(dòng)維護(hù)的內(nèi)存,可以根據(jù)實(shí)際使用需求來決定分配和釋放內(nèi)存的策略很多時(shí)候,這不是什么大不了的事。大多數(shù)場景并不會(huì)對性能要求那么苛刻,反而更多地?fù)?dān)心管理內(nèi)存的麻煩。 作者:Lin Clark 譯者:Cody Chan 原帖鏈接:A cartoon intro to ArrayBuffers and SharedArrayBu...
摘要:你可以從內(nèi)存中直接拿東西,也可以直接往內(nèi)存里存東西當(dāng)你把或者其它語言編譯為時(shí),編譯工具會(huì)在里增加一些輔助代碼。 作者:Lin Clark 譯者:Cody Chan 原帖鏈接:A crash course in memory management 這是圖解 SharedArrayBuffers 系列的第一篇: 內(nèi)存管理碰撞課程 圖解 ArrayBuffers 和 SharedA...
2017-10-01 前端日報(bào) 精選 網(wǎng)頁保存為圖片及高清截圖的優(yōu)化方法前端最佳實(shí)踐(一)——DOM操作Vue 2.0學(xué)習(xí)筆記:v-bindReact Router v4 之代碼分割:從放棄到入門js實(shí)用的十個(gè)小技巧Netflix/falcor: A JavaScript library for efficient data fetchinglllyasviel/style2paints: ske...
摘要:前端日報(bào)精選十問幫你理清前端工程師及大前端團(tuán)隊(duì)的成長問題譯讀完細(xì)則之后學(xué)到的件事掘金怎么寫一個(gè)組件庫一眾成翻譯還有這操作一個(gè)能生成思維導(dǎo)圖的開源搜索引擎知乎專欄中文前端推薦第天值得收藏的基礎(chǔ)教程知乎專欄第期沒有的一天轉(zhuǎn)載中回調(diào)地 2017-06-15 前端日報(bào) 精選 十問sofish:幫你理清前端工程師及大前端團(tuán)隊(duì)的成長問題![譯] 讀完 flexbox 細(xì)則之后學(xué)到的 11 件事 -...
閱讀 1411·2021-10-08 10:04
閱讀 733·2021-09-07 09:58
閱讀 2912·2019-08-30 15:55
閱讀 2424·2019-08-29 17:21
閱讀 2126·2019-08-28 18:04
閱讀 3075·2019-08-28 17:57
閱讀 715·2019-08-26 11:46
閱讀 2228·2019-08-23 17:20