客戶端JavaScript的執行緒池設計詳解
阿新 • • 發佈:2022-01-25
目錄
- 1.介紹:
- 2.準備工作:
- 3.測試spark-md5是否正常工作:
- 4.執行緒池設計
- 5.spark-md5對檔案進行md5編碼
- 6.大量檔案進行MD5加密並使用執行緒池優化
- 總結
1.介紹:
本打算在客戶端進行機器學習演算法計算時應用執行緒池來優化,就像()演示的神經網路。但是由於各種原因不了了之了。本次遇到了一個新的問題,客戶端的MD5運算也是耗時操作,如果同時對多個字串或檔案進行MD5加密就可以使用執行緒池來優化。
2.準備工作:
到npm官網搜尋spark-md5,到其倉庫下載spark-md5.。該js檔案支援AMD,CommonJS和web工作執行緒的模組系統,我們在實現執行緒池時,執行緒工作程式碼交給web工作執行緒處理。
3.測試spark-md5是否正常工作:
建立一個,再建立一個worker.js用於儲存工作執行緒的程式碼。以下述程式碼測試,如果成功輸出MD5編碼,那麼準備工作完成。
客戶端網頁程式碼
<script> let worker = new Worker("worker.js") worker.postMessage("Danny") worker.onmessage = function({data}) { console.log(data) worker.terminate() } </script>
工作執行緒程式碼
self.importScripts("spark-md5.js") self.onmessage = function({data}) { self.postMessage(self.SparkMD5.hash(data)) }
4.執行緒池設計
1. 目標:本次執行緒池設計的目標是初始建立n個初始執行緒,能夠滿足任意個執行緒請求,超出n的請求並不丟棄,而是等待到出現空閒執行緒後再分配之。
2. 基本設計思路:為了基本滿足上述目標,至少要有一個執行緒分配功能,一個執行緒回收功能。
3. 執行緒分配功能設計:
- 執行緒池滿指的是執行緒池已經沒有可用空閒執行緒
- 通知物件是一個不可逆狀態機,可以用Promise物件來實現
- 阻塞請求佇列儲存Promise物件的resolve方法即可
- 儲存執行緒池中的執行緒使用陣列即可,陣列每個元素是一個物件,包括執行緒和執行緒狀態
- 返回給使用者的可用執行緒還需要有執行緒在陣列中的下標,線上程釋放中會用到
4. 執行緒釋放功能設計:
- 執行緒釋放功能需要接收一個引數,為執行緒的標識,3中設計該標識為陣列下標
- 當執行緒釋放後,檢視阻塞請求佇列是否為空,如果不為空,說明有被阻塞的執行緒請求,此時令隊首元素出隊即可,執行resolve()通知物件的狀態變更為Fulfilled
5. 實現執行緒池:
class MD5Pool { // worker用於儲存執行緒 worker = [] // status是執行緒池狀態 status = "Idle" // 阻塞請求佇列 blockRequestQueue = [] // size為使用者希望的執行緒池的容量 constructor(size) { for(let i = 0; i < size; i ++) this.worker.push({ worker: new Worker("worker.js"),status: "Idle" }) } // 執行緒池狀態更新函式 statusUpdate() { let sum = 0 this.worker.forEach(({ status }) => { if(status === "Busy") sum ++ }) if(sum === this.worker.length) this.status = "Busy" else this.status = "Idle" } // 執行緒請求方法 assign() { if(this.status !== "Busy") { // 此時執行緒池不滿,遍歷執行緒,尋找一個空閒執行緒 for (let i = 0; i < this.worker.length; i++) if (this.worker[i].status === "Idle") { // 該執行緒空閒,更新狀態為忙碌 this.worker[i].status = "Busy" // 更新執行緒池狀態,如果這是最後一個空閒執行緒,那麼執行緒池狀態變為滿 this.statusUpdate() // 返回給使用者該執行緒,和該執行緒的標識,標識用陣列下標表示 return { worker: this.worker[i].worker,index: i } } } else { // 此時執行緒池滿 let resolve = null // 建立一個通知物件 let promise = new Promise(res => { // 取得通知物件的狀態改變方法 resolve = res }) // 通知物件的狀態改變方法加入阻塞請求佇列 this.blockRequestQueue.push(resolve) // 返回給請求者執行緒池已滿資訊和通知物件 return { info: "full",wait: promise } } } // 執行緒釋放方法,接收一個引數為執行緒標識 release(index) { this.worker[index].status = "Idle" // 阻塞請求佇列中的第一個請求出隊,佇列中儲存的是promise的resolve方法,此時執行,通知請求者已經有可用的執行緒了 if(this.blockRequestQueue.length) // 阻塞請求佇列隊首出列,並執行通知物件的狀態改變方法 this.blockRequestQueue.shift()() // 更新執行緒池狀態,此時一定空閒 this.status = "Idle" } }
5.spark-md5對檔案進行md5編碼
說明:
在3的測試中spark-md5只是對簡單字串進行MD5編碼,並非需要大量運算的耗時操作。spark-md5可以對檔案進行MD5編碼,耗時較多,實現如下。
注意:
spark-md5對檔案編碼時必須要對檔案進行切片後再加密整合,否則不同檔案可能會有相同編碼。詳情見github或npm。
// 在工作執行緒中引入spark-md5 self.importScripts("spark-md5.js") let fd = new FileReader() let spark = new self.SparkMD5.ArrayBuffer() // 接收主執行緒發來的訊息,是一個檔案 self.onmessage = function(event) { // 獲取檔案 let chunk = event.data // spark-md5要求計算檔案的MD5必須切片計算 let chunks = fileSlice(chunk) // 計算MD5編碼 load(chunks) } // 切片函式 function fileSlice(file) { let pos = 0 let chunks = [] // 將檔案平均切成10分計算MD5 const SLICE_SIZE = Math.ceil(file.size / 10) while(pos < file.size) { // slice可以自動處理第二個引數越界 chunks.push(file.slice(pos,pos + SLICE_SIZE)) pos += SLICE_SIZE } return chunks } // MD5計算函式 asynYICMbieTqc function load(chunks) { for(let i = 0; i < chunks.length; i ++) { fd.readAsArrayBuffer(chunks[i]) // 在這裡希望節約空間,因此複用了FileReader,而不是每次迴圈新建立一個FileReader。需要等到FileReader完成read後才可以進行下一輪複用,因此用await阻塞。 await new Promise(res => { fd.onload = function(event) { spark.append(event.target.result) if(i === chunks.length - 1) { self.postMessage(spark.end()) } res() } }) } }
6.大量檔案進行MD5加密並使用執行緒池優化
下面的測試程式碼就是對上文所述的拼接
網頁程式碼
<input id="input" type="file" multiple onchange="handleChanged()"/>
<body>
<script>
class MD5Pool {
worker = []
status = "Idle"
blockRequestQueue = []
constructor(size) {
for(let i = 0; i < size; i ++)
this.worker.push({
worker: new Worker("worker.js"),status: "Idle"
})
}
statusUpdate() {
let sum = 0
this.worker.forEach(({ status }) => {
if(status === "Busy")
sum ++
})
if(sum === this.worker.length)
this.status = "Busy"
else
this.status = "Idle"
}
assign() {
if(this.status !== "Busy") {
for (let i = 0; i < this.worker.length; i++)
if (this.worker[i].status === "Idle") {
this.worker[i].status = "Busy"
this.statusUpdate()
return {
worker: this.worker[i].worker,index: i
}
}
}
else {
let resolve = null
let promise = new Promise(res => {
resolve = res
})
this.blockRequestQueuhttp://www.cppcns.come.push(resolve)
return {YICMbieTq
info: "full",wait: promise
}
}
}
release(index) {
this.worker[index].status = "Idle"
// 阻塞請求佇列中的第一個請求出隊,佇列中儲存的是promise的resolve方法,此時執行,通知請求者已經有可用的執行緒了
if(this.blockRequestQueue.length)
this.blockRequestQueue.shift()()
this.status = "Idle"
}
}
// input點選事件處理函式
function handleChanged() {
let files = event.target.files
// 建立一個大小為2的MD5計算執行緒池
let pool = new MD5Pool(2)
// 計算切片檔案的MD5編碼
Array.prototype.forEach.call(files,file => {
getMD5(file,pool)
})
}
// 獲取檔案的MD5編碼的函式,第一個引數是檔案,第二個引數是MD5執行緒池
async function getMD5(chunk,pool) {
let thread = pool.assign()
// 如果info為full,那麼說明執行緒池執行緒已被全部佔用,需要等待
if(thread.info === "full") {
// 獲取執行緒通知物件
let wait = thread.wait
// 等到wait兌現時說明已經有可用的執行緒了
await wait
thread = pool.assign()
let { worker,index } = thread
worker.postMessage(chunk)
worker.onmessage = function (event) {
console.log(event.data)
pool.release(index)
}
} else {
let { worker,index } = thread
worker.postMessage(chunk)
worker.onmessage = function (event) {
console.log(event.data)
pool.release(index)
}
}
}
</script>
</body>
工作執行緒程式碼
self.importScripts("spark-md5.js")
let fd = new FileReader()
let spark = new self.SparkMD5.ArrayBuffer()
self.onm客棧essage = function(event) {
// 獲取檔案
let chunk = event.data
// spark-md5要求計算檔案的MD5必須切片計算
let chunks = fileSlice(chunk)
// 計算MD5編碼
load(chunks)
}
// 切片函式
function fileSlice(file) {
let pos = 0
let chunks = []
// 將檔案平均切成10分計算MD5
const SLICE_SIZE = Math.ceil(file.size / 10)
while(pos < file.size) {
// slice可以自動處理第二個引數越界
chunks.push(file.slice(pos,pos + SLICE_SIZE))
pos += SLICE_SIZE
}
return chunks
}
// MD5計算函式
async function load(chunks) {
for(let i = 0; i < chunks.length; i ++) {
fd.readAsArrayBuffer(chunks[i])
// 在這裡希望節約空間,因此複用了FileReader,而不是每次迴圈新建立一個FileReader。需要等到FileReader完成read後才可以進行下一輪複用,因此用await阻塞。
await new Promise(res => {
fd.onload = function(event) {
spark.append(event.target.result)
if(i === chunks.length - 1) {
self.postMessage(spark.end())
}
res()
}
})
}
}
隨機選取18個檔案進行MD5編碼,結果如下
總結
本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注我們的更多內容!