日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

      歡迎訪問 生活随笔!

      生活随笔

      當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

      java

      Javascript 多线程编程​的前世今生

      發(fā)布時(shí)間:2024/2/28 java 38 豆豆
      生活随笔 收集整理的這篇文章主要介紹了 Javascript 多线程编程​的前世今生 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

      作者:jolamjiang,騰訊 WXG 前端開發(fā)工程師

      一篇關(guān)于 Web Worker、SharedArrayBuffer、Atomics 的文章。

      為什么要多線程編程

      大家看到文章的標(biāo)題《Javascript 多線程編程》可能立馬會(huì)產(chǎn)生疑問:Javascript 不是單線程的嗎?Javascript IO 阻塞和其他異步的需求(例如 setTimeout, Promise, requestAnimationFrame, queueMicrotask 等)不是通過事件循環(huán)(Event Loop)來解決的嗎?

      沒有錯(cuò),Javascript 的確是單線程的,阻塞和其他異步的需求的確是通過實(shí)現(xiàn)循環(huán)來解決的,但是這套機(jī)制當(dāng)線程需要處理大規(guī)模的計(jì)算的時(shí)候就不大適用了,試想一下一下的場景:

    1. 你需要實(shí)現(xiàn)對(duì)文件的加解密。

    2. 你的 VirtualDom 樹有很多元素(例如上萬個(gè)),你需要對(duì)這棵樹進(jìn)行 Diff 操作。

    3. 你需要在瀏覽器“挖礦”。

    4. 上面這些場景都會(huì)阻塞主線程,也就是當(dāng)進(jìn)行這些操作的時(shí)候,你的頁面是卡住的,設(shè)置當(dāng)頁面卡住一段時(shí)間之后,Chrome 等瀏覽器或者操作系統(tǒng)會(huì)建議你 Kill 掉整個(gè) Tab 或者進(jìn)程。這顯然不是我們想看到的事情。正因?yàn)檫@些場景的存在,瀏覽器提出了 W3C 在 2013 年提出了 Web Worker 草案,這個(gè)草案的提出就是為了解決上述這些問題。

      為了讓大家感受 JS 多線程能夠干什么,筆者寫了一個(gè)基于 Web Worker(線程)、ShareArrayBuffer(共享內(nèi)存)、Atomics(鎖)等 Web API 的在前端壓縮和解壓文件(基于 DEFLATE 算法)的 demo:

      查看視頻,點(diǎn)擊 Demo 的在線地址 自己來試試吧。

      Web Worker

      Chrome 瀏覽器中每個(gè) Tab 都是一個(gè)進(jìn)程,每個(gè)進(jìn)程都會(huì)有一個(gè)主線程,網(wǎng)頁的渲染(Style, Layout, Paint, Composite)會(huì)在主線程進(jìn)行操作。主線程可以發(fā)起多個(gè) Web Worker,Web Worker 對(duì)應(yīng)“線程”的概念。

      每個(gè) Web Worker 都對(duì)應(yīng)一個(gè)腳本文件,主線程可以通過像以下的代碼去發(fā)起多個(gè) Web Worker,并且通過基于事件的 API 與 Web Worker 通信:

      main.js

      let?worker?=?new?Worker("work.js"); worker.postMessage("Hello?World"); worker.onmessage?=?function?(event)?{console.log("Received?message?"?+?event.data); }

      Web Worker 也通過相應(yīng)的實(shí)現(xiàn) API 與主線程進(jìn)行通信

      worker.js

      this.addEventListener("message",?function?(e)?{this.postMessage("You?said:?"?+?e.data); },?false);

      Web Worker 通訊的效率與同步問題

      主線程與 Web Worker 通過 postMessage(data: any) 通信的時(shí)候,data 會(huì)先被 copy 一份再傳給 Web Worker;同樣地,當(dāng) Web Worker 通過 postMessage(data: any) 與主線程通信的時(shí)候,data 也會(huì)同樣先被 copy 一份再傳給主線程。

      這樣做顯然會(huì)導(dǎo)致通信上的效率問題,試想一下你需要在 Web Worker 里面解壓一個(gè) 1G 大小的問題,你需要把整個(gè) 1G 的文件 copy 到 Web Worker 里,Web Worker 解壓完這個(gè) 1G 文件后,再把解壓完的文件 copy 回主線程里。

      SharedArrayBuffer

      為了解決通訊效率問題,瀏覽器提出了 ShareArrayBuffer,ShareArrayBuffer 基于 ArrayBuffer 和 TypedArray API。ArrayBuffer 對(duì)應(yīng)一段內(nèi)存(二進(jìn)制內(nèi)容),為了操作這段內(nèi)存,瀏覽器需要提供一些視圖(Int8Array 等),例如可以把這段內(nèi)存當(dāng)做每 8 位一個(gè)單元的 byte 數(shù)組,每 16 位一個(gè)單元的 16 位有符號(hào)數(shù)數(shù)組。

      注意:ArrayBuffer 中的二進(jìn)制流被翻譯成各種視圖的時(shí)候采用小端還是大端是由具體硬件決定的,絕大部分情況下是采用小端字節(jié)順序。

      這段內(nèi)存可以在不同的 Worker 之間共享,但是內(nèi)存的共享又會(huì)產(chǎn)生另外的問題,也就是競爭的問題(race onditions):

      計(jì)算機(jī)指令對(duì)內(nèi)存操作進(jìn)行運(yùn)算的時(shí)候,我們可以看做分兩步:一是從內(nèi)存中取值,二是運(yùn)算并給某段內(nèi)存賦值。當(dāng)我們有兩個(gè)線程對(duì)同一個(gè)內(nèi)存地址進(jìn)行 +1 操作的時(shí)候,假設(shè)線程是先后順序運(yùn)行的,為了簡化模型,我們可以如下圖表示:

      上面兩個(gè)線程的運(yùn)行結(jié)果也符合我們的預(yù)期,也即線程分別都對(duì)同一地址進(jìn)行了 +1 操作,最后得到結(jié)果 3。但因?yàn)閮蓚€(gè)線程是同時(shí)運(yùn)行的,往往會(huì)發(fā)生下圖所表示的問題,也即讀取與寫入可能不在一個(gè)事務(wù)中發(fā)生:

      這種情況就叫做競爭問題(Race Condition)。

      Atomics

      為了解決上述的競爭問題,瀏覽器提供了 Atomics API,這組 API 是一組原子操作,可以將讀取和寫入綁定起來,例如下圖中的 S1 到 S3 操作就被瀏覽器封裝成 Atomics.add() 這個(gè) API,從而解決競爭問題。

      Atomics API 具體包含:

    5. Atomics.add()

    6. Atomics.and()

    7. Atomics.compareExchange()

    8. Atomics.exchange()

    9. Atomics.isLockFree()

    10. Atomics.load()

    11. Atomics.notify()

    12. Atomics.or()

    13. Atomics.store()

    14. Atomics.sub()

    15. Atomics.wait()

    16. Atomics.xor()

    17. 有了這套 API,我們可以實(shí)現(xiàn)像 Golang 中的 Golang Synchronization Primitives 的功能。Mutex 和 Cond 的實(shí)現(xiàn)會(huì)在下面介紹。

      WebAssembly

      有了 SharedArrayBuffer 和 Atomics 能力之后,證明瀏覽器能夠提供內(nèi)存共享和鎖的實(shí)現(xiàn)了,也就是說 WebAssembly 線程在瀏覽器機(jī)制上能夠高效地得到保證。

      其實(shí)我嚴(yán)重懷疑 SharedArrayBuffer 和 Atomics 是為了支持 WebAssembly 才把 API 順便提供給 JS Runtime 的,因?yàn)槟壳盀橹箾]有看到 ES 有比較豐富的關(guān)于鎖的草案(例如像 Java 中的 synchronized 關(guān)鍵字)。

      Mutext 和 Cond 的實(shí)現(xiàn)

      上面提到了,基于 ShareArrayBuffer 和 Atomics 可以開發(fā)像 Golang Synchronization Primitives 一樣的 API,下面介紹一下 Mutex 和 Cond 的實(shí)現(xiàn)。實(shí)現(xiàn)的介紹是基于 Mozzila Javascript 編譯器工程師 Lars T Hansen 實(shí)現(xiàn)關(guān)于鎖的庫。

      Mutex

      首先說一下 Mutex 的功能,Mutex 的 API 大概是這樣的:

      let?mutex?=?new?Lock(shareArrayBuffer,?...); mutex.lock(); doSomething(); mutex.unlock();

      Mutex 可以保證 lock() 和 unlock() 之間的代碼代碼不會(huì)被打斷。下面是介紹具體實(shí)現(xiàn):

      首先定義 Mutex 的三個(gè)狀態(tài)以及對(duì)應(yīng)的狀態(tài)機(jī)

    18. UNLOCK: 未鎖定

    19. LOCKED: 被鎖定

    20. WAITED: 被鎖定且大于等于 1 個(gè)線程在等待該鎖

    21. 對(duì)于 Worker 線程來說 Mutex 的每個(gè)狀態(tài)都可能是初始態(tài),狀態(tài)與狀態(tài)間扭轉(zhuǎn)會(huì)產(chǎn)生一些操作且進(jìn)入下一狀態(tài):

      加鎖 lock()

    22. 初始狀態(tài)為UNLOCK: 鎖未被搶占,將狀態(tài)扭轉(zhuǎn)為 LOCKED,線程進(jìn)行后續(xù)操作。

    23. 初始狀態(tài)為LOCKED: 鎖已被搶占,將狀態(tài)扭轉(zhuǎn)為 WAITED,并將線程設(shè)置為等待態(tài),并將線程設(shè)置為當(dāng)鎖的狀態(tài)不為 WAITED 的時(shí)候可能被喚醒,一旦被喚醒則該線程擁有鎖,線程進(jìn)行后續(xù)操作。

    24. 初始狀態(tài)為WAITED: 鎖已被搶占,并將線程設(shè)置為等待態(tài),并將線程設(shè)置為當(dāng)鎖的狀態(tài)不為 WAITED 的時(shí)候可能被喚醒,一旦被喚醒則該線程擁有鎖,線程進(jìn)行后續(xù)操作。

    25. 釋放 unlock()

      1.初始狀態(tài)為LOCKED: 鎖被搶占且未被等待,將狀態(tài)扭轉(zhuǎn)為 UNLOCK,線程進(jìn)行后續(xù)操作。

    26. 初始狀態(tài)為WAITED: 鎖被搶占且被等待,將狀態(tài)扭轉(zhuǎn)為 LOCKED,喚醒一個(gè)在等待態(tài)的線程,線程進(jìn)行后續(xù)操作。

    27. 上面描述的邏輯的對(duì)應(yīng)的代碼如下:

      //?lock Lock.prototype.lock?=?function?()?{const?iab?=?this._iab;const?stateIdx?=?this._ibase;let?c;if?((c?=?Atomics.compareExchange(iab,?stateIdx,?0,?1))?!=?0)?{do?{if?(c?==?2?||?Atomics.compareExchange(iab,?stateIdx,?1,?2)?!=?0)Atomics.wait(iab,?stateIdx,?2);}?while?((c?=?Atomics.compareExchange(iab,?stateIdx,?0,?2))?!=?0);} }//?unlock Lock.prototype.unlock?=?function?()?{const?iab?=?this._iab;const?stateIdx?=?this._ibase;let?v0?=?Atomics.sub(iab,?stateIdx,?1);//?Wake?up?a?waiter?if?there?are?anyif?(v0?!=?1)?{Atomics.store(iab,?stateIdx,?0);Atomics.notify(iab,?stateIdx,?1);} }

      可以看到鎖的實(shí)現(xiàn)用到了 Atomics.compareExchange() 和 Atomics.wait()(相當(dāng)于 Linux 中的 futex)兩個(gè)原子操作。

      Cond

      Cond 是基于 Mutex 實(shí)現(xiàn)的,它的大致功能是持有鎖的情況下可進(jìn)行兩種操作:

    28. wait(): 本線程進(jìn)度進(jìn)入等待態(tài),并且被喚醒的時(shí)候重新持有鎖。

    29. notifyOne(): 喚醒一個(gè)正在等待態(tài)的線程。

    30. 具體使用方法如下:

      //?thread?A var?msg?=?new?Int32Array(sab,?msgLoc,?1); lock.lock(); while?(msg[0]?<?numWorkers)cond.wait(); lock.unlock();//?thread?B,?C,?D,?E,?… var?msg?=?new?Int32Array(sab,?msgLoc,?1); lock.lock(); msg[0]++; cond.notifyOne(); lock.unlock();

      由于 Cond 是基于 Mutex,前置條件是持有鎖,后置條件是釋放鎖,你可以看做 Cond 只有兩個(gè)狀態(tài):

    31. NORMAL: 非等待態(tài),調(diào)用 wait() 轉(zhuǎn)化為 WAITED 狀態(tài),并把線程設(shè)置為等待態(tài),并且被喚醒的時(shí)候重新持有鎖,然后進(jìn)行后續(xù)操作。

    32. WAITED: 等待態(tài)(不對(duì)應(yīng)上述 Lock 的 WAITED 態(tài)),調(diào)用 notifyOne() 將狀態(tài)設(shè)置為 NORMAL 態(tài),重新喚醒一個(gè)處于等待態(tài)的線程,然后進(jìn)行后續(xù)操作。

    33. 異步鎖

      上述介紹的鎖都是同步的,Atomics.wait 不能在主線程使用,在主線程使用的話瀏覽器會(huì)拋出異常:

      Uncaught TypeError: Atomics.wait cannot be called in this context

      所以我們需要設(shè)計(jì)所謂的”異步鎖“,所謂的異步鎖原理很簡單,就是將同步鎖里面的 Atomics.wait() 操作交給一個(gè)新的線程,主線程和這個(gè)線程通過事件通信來異步化這里的操作。具體實(shí)現(xiàn)可以參照這個(gè)文件)。

      demo 實(shí)現(xiàn)

      介紹完上述的知識(shí)之后,就可以用相關(guān)的 API 就可以實(shí)現(xiàn)我們的 demo 了,首先畫一下我們 demo 的架構(gòu)圖:

      如圖所示,在線解壓縮這個(gè) demo 主要分為兩個(gè)線程:

    34. 主線程:負(fù)責(zé)調(diào)用 Dom API 等,主要負(fù)責(zé) UI 更新。

    35. 工作線程:負(fù)責(zé)文件的壓縮/解壓。

    36. 兩個(gè)線程間的通信是通過讀寫兩段共享內(nèi)存來實(shí)現(xiàn)的,對(duì)于共享內(nèi)存的訪問,通過鎖來解決競爭問題。需要注意的是,主線程的寫緩存也即工作線程的讀緩存,反之亦然。

      demo 的具體實(shí)現(xiàn)可以參照 demo 的 Github 地址。

      目前多線程編程的不足

      目前只通過瀏覽器提供的 API 來進(jìn)多線程開發(fā)的話成本非常大,主要有兩方面問題:

      過于底層的 API

    37. 需要你實(shí)現(xiàn)語言級(jí)、或者系統(tǒng)級(jí)的 lock API,參照 Golang 的 lock API。

    38. 沒有語法上的支持,例如 Java synchronized 關(guān)鍵字等。

    39. 普通的 Javascript Object 無法共享

      這其實(shí)也是 API 過于底層的另一方面的體現(xiàn),也就是說對(duì) JS 對(duì)象進(jìn)行內(nèi)存共享的話,你需要開辟一段 SharedArrayBuffer,然后在此之上實(shí)現(xiàn)對(duì) JS 對(duì)象的序列化、反序列化、更新等操作,實(shí)現(xiàn)成本也是比較大的。

      事實(shí)上我們也不應(yīng)該輕易手動(dòng)實(shí)現(xiàn)相關(guān)的庫或者功能,因?yàn)橄嚓P(guān)領(lǐng)域的問題非常復(fù)雜、需要仔細(xì)的設(shè)計(jì)和實(shí)現(xiàn)。例如我們可以先使用下面這兩個(gè)庫:

    40. parlib-simple: 這個(gè)庫里面有類似于 Golang 里面 channel 一樣的 API。

    41. js-lock-and-condition: 這里庫有 Mutex 和 Cond 實(shí)現(xiàn)。

    42. 總結(jié)

      瀏覽器提供給了我們進(jìn)行多線程的能力,例如 PWA 或者 WebAseembly 與 JS 混用等場景都會(huì)用到上述的機(jī)制,如果你想實(shí)現(xiàn)一個(gè)高性能的網(wǎng)頁客戶端程序(例如 Figma 一樣的殺手級(jí)應(yīng)用),你最好也用上上述的機(jī)制。值得注意的是,用了鎖可能會(huì)降低你的程序的性能,具體要看線程切換和等待是的成本是否能夠抵消內(nèi)存拷貝的成本,例如 demo 完全可以改成無鎖的,代價(jià)將文件內(nèi)容拷貝到共享線程,并把工作線程的內(nèi)容拷貝回主線程。

      雖然上面建議不要輕易實(shí)現(xiàn)自己的庫,例如上面的 lock 代碼短短幾行,但是其中的推導(dǎo)可以足夠?qū)懯畮醉摰?Paper 了,但是這里的基礎(chǔ)能力很匱乏,據(jù)筆者了解,TC39 提案中鮮少出現(xiàn)關(guān)于多線程編程的提案,目前僅發(fā)現(xiàn)以下這個(gè):

    43. proposal-atomics-wait-async

    44. 但是,如果自信有能力和時(shí)間建設(shè)這些基礎(chǔ)能力的話,這個(gè)領(lǐng)域的確是“廣闊天地,大有作為”,特別是如果你的項(xiàng)目準(zhǔn)備用 WebAseembly 和 JS 混用的情況(例如 Figma 就是用了 WebAssembly 和 React)。

      另外微信支付行業(yè)繳費(fèi)、微信支付支付分行業(yè)招聘前端和后臺(tái)工程師,在這里你可以:

      掛個(gè)微信支付招聘

      • 微信支付行業(yè)繳費(fèi)開發(fā)工程師(深圳)

      • 微信支付分行業(yè)Web前端工程師

      • 微信支付分行業(yè)后臺(tái)開發(fā)工程師(深圳)

      總結(jié)

      以上是生活随笔為你收集整理的Javascript 多线程编程​的前世今生的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

      如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。