一、需求
JS著名的Event Loop限制了使用多線程的想象力北秽,這對于高并發(fā)IO操作是不錯的選擇,但對于高并發(fā)的CPU型運算狼牺,必然是捉襟見肘羡儿。
Event Loop
二、早期的NodeJs解決之道
依賴于強(qiáng)大的V8引擎是钥,nodeJS可以借助于對系統(tǒng)底層的調(diào)用掠归,利用子進(jìn)程完成對并發(fā)計算的需求,體現(xiàn)在child_process和cluster這兩個模塊悄泥。
但進(jìn)程的內(nèi)存區(qū)不可共享虏冻,進(jìn)程切換開銷,及通過Binding Bridge的通信弹囚,將極大的限制NodeJS的工作效率厨相!
三、ES6 的MessageChannel 是解決問題的突破口
MessageChannel和MessagePort
- 演示代碼(瀏覽器環(huán)境):
var channel=new MessageChannel();
let book={id:1}
//默認(rèn):book將對序列化的方式進(jìn)行發(fā)送
//port1發(fā)送時,由port2時行接收
channel.port1.postMessage(book);
//默認(rèn):event.data,是book對象的"深克隆對象"
channel.port2.onmessage=function (event) {
console.log(event.data.id) //show 1
}
- 分析:
此時蛮穿,所有的處理工作都是在event loop的線程中執(zhí)行庶骄,如果channel的兩頭分別是兩個不同的線程,就可以完成多線程的通信重任了践磅!
- 未完成的筆記:
后續(xù)會用一篇文章分析三種不同形式的postMessage參數(shù)情況单刁。
四、先談下Web Workers
由ES6引入的子線程的概念府适,可以使用new Worker(url)羔飞,將url指向的js代碼,放入子線程中執(zhí)行檐春,詳見代碼(以瀏覽器環(huán)境為例):
- 在html文件中
<script>
//啟動Worker子線程逻淌,并傳入?yún)?shù)
var worker=new Worker('sub.js',{name:'john'})
//進(jìn)行異常處理
worker.onerror=function (err) {
console.log(err.message)
}
</script>
- 被子線程加載的代碼 sub.js
//這里的代碼將在子線程中執(zhí)行
//注意:這里不是能訪問document,window對象的,但可以訪問location,navigator對象
var name=self.name //獲取啟動時傳入的信息
throw new Error('name is recieved!') //將在主線程中的worker.onerror中接收到
self.close() //強(qiáng)行關(guān)閉子線程(并不需要顯示調(diào)用)
- 運行結(jié)果
Uncaught Error: john is recieved!
五疟暖、父子線程通信
- 原理:利用一個匿名管理(MessageChannel),利用其port1,port2完成通訊
- 代碼(以瀏覽器環(huán)境為例)
html中
<script>
//啟動Worker子線程卡儒,并傳入?yún)?shù)
var worker=new Worker('sub.js',{name:'john'})
//父線程向子線程發(fā)送
worker.postMessage({name:"good js"})
//等待子線程發(fā)送過來的數(shù)據(jù)
worker.onmessage=function (event) {
console.log(event.data)
}
</script>
sub.js(子線程)
//這里的代碼將在子線程中執(zhí)行
var name=self.name //獲取啟動時傳入的信息
self.onmessage=function(event){
//這里獲取的是一個深克隆對象
var book=event.data;
book.id=1
//子線程向父線程發(fā)送數(shù)據(jù)
self.postMessage(book)
}
- 運行結(jié)果
{name: "good js", id: 1}
六、postMessage的參數(shù):
- 默認(rèn)情況下誓篱,會對參數(shù)時行序列化操作(要在接收方產(chǎn)生一個深克隆對象)朋贬,但其能力要遠(yuǎn)強(qiáng)于JSON.stringify,表現(xiàn)在以下:
(1)可以處理循環(huán)引用問題
(2)處理一些js內(nèi)置對象窜骄,如Set,Regexp等對象
(3)能處理ArrayBuffer
(4)能處理一些C++原生的對象锦募,如:MessagePort
但要注意:參數(shù)對象,如果帶有方法時邻遏,處理時會拋出異常糠亩。 - 當(dāng)參數(shù)的類型為SharedArrayBuffer時,會將對象本身進(jìn)行共享(共享內(nèi)存區(qū))
const sharedUint8Array = new Uint8Array(new SharedArrayBuffer(4));
worker.postMessage(sharedUint8Array);
- 當(dāng)postMessage加上可選的第二個參數(shù)(TransferList)時准验,也表示“共享內(nèi)存區(qū)”赎线,但在TransferList加以標(biāo)明的對象,在發(fā)送完畢后糊饱,是不可以被使用的垂寥!
const uint8Array = new Uint8Array([ 1, 2, 3, 4 ]);
worker.postMessage(uint8Array, [ uint8Array.buffer ]);
此時uint8Array
會以內(nèi)存區(qū)的形式直接共享,但由于其在第二個參數(shù)中加以注明(成為TranserList的一部分)另锋,所以其在主線程中滞项,是不可以再繼續(xù)使用的。
- TransferList的中的對象類型:
只能是ArrayBuffer和MessagePort兩種類型夭坪。
七文判、利用MessageChannel和MessagePort完成子線程間的通信。
- 思路:在主線程中室梅,將MessageChannel的port1和port2戏仓,分別以postMessage的參數(shù)發(fā)送到兩個子線程疚宇,然后子線程利用收到的port,對channel進(jìn)行發(fā)送和接收赏殃,從而完成子線程間的通信敷待。
- 代碼:
html:
<script>
//子線程通信用的channel
var channel=new MessageChannel();
//分別啟動兩個子線程
var worker1=new Worker('sub1.js')
var worker2=new Worker('sub2.js')
//將port1和port2分別傳遞給子線程
worker1.postMessage({port:channel.port1},[channel.port1])
worker2.postMessage({port:channel.port2},[channel.port2])
</script>
sub1.js和sub2.js
//接收從父線程發(fā)送過來的port
self.onmessage=function (event) {
//利用port1進(jìn)行發(fā)送到channel(此時port2上出現(xiàn)數(shù)據(jù))
event.data.port.postMessage("I am sub1 worker!")
//利用port1監(jiān)聽channel(等待從port2發(fā)送過來的數(shù)據(jù))
event.data.port.onmessage=function (event) {
console.log("in sub1:"+event.data)
}
}
self.onmessage=function (event) {
event.data.port.postMessage("I am sub2 worker!")
event.data.port.onmessage=function (event) {
console.log("in sub2:"+event.data)
}
}
- 運行結(jié)果:
in sub2:I am sub1 worker!
in sub1:I am sub2 worker!