客戶端JavaScript的線程池設計詳解
1.介紹:
本打算在客戶端JavaScript進行機器學習算法計算時應用線程池來優(yōu)化,就像()演示的神經(jīng)網(wǎng)絡。但是由于各種原因不了了之了。本次遇到了一個新的問題,客戶端的MD5運算也是耗時操作,如果同時對多個字符串或文件進行MD5加密就可以使用線程池來優(yōu)化。
2.準備工作:
到npm官網(wǎng)搜索spark-md5,到其github倉庫下載spark-md5.js。該js文件支持AMD,CommonJS和web工作線程的模塊系統(tǒng),我們在實現(xiàn)線程池時,線程工作代碼交給web工作線程處理。



3.測試spark-md5是否正常工作:
創(chuàng)建一個網(wǎng)頁,再創(chuàng)建一個worker.js用于保存工作線程的代碼。以下述代碼測試,如果成功輸出MD5編碼,那么準備工作完成。
客戶端網(wǎng)頁代碼
<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. 目標:本次線程池設計的目標是初始創(chuàng)建n個初始線程,能夠滿足任意個線程請求,超出n的請求并不丟棄,而是等待到出現(xiàn)空閑線程后再分配之。
2. 基本設計思路:為了基本滿足上述目標,至少要有一個線程分配功能,一個線程回收功能。
3. 線程分配功能設計:
- 線程池滿指的是線程池已經(jīng)沒有可用空閑線程
- 通知對象是一個不可逆狀態(tài)機,可以用Promise對象來實現(xiàn)
- 阻塞請求隊列存儲Promise對象的resolve方法即可
- 存儲線程池中的線程使用數(shù)組即可,數(shù)組每個元素是一個對象,包括線程和線程狀態(tài)
- 返回給用戶的可用線程還需要有線程在數(shù)組中的下標,在線程釋放中會用到

4. 線程釋放功能設計:
- 線程釋放功能需要接收一個參數(shù),為線程的標識,3中設計該標識為數(shù)組下標
- 當線程釋放后,查看阻塞請求隊列是否為空,如果不為空,說明有被阻塞的線程請求,此時令隊首元素出隊即可,執(zhí)行resolve()通知對象的狀態(tài)變更為Fulfilled

5. 實現(xiàn)線程池:
class MD5Pool {
// worker用于存儲線程
worker = []
// status是線程池狀態(tài)
status = "Idle"
// 阻塞請求隊列
blockRequestQueue = []
// size為用戶希望的線程池的容量
constructor(size) {
for(let i = 0; i < size; i ++)
this.worker.push({
worker: new Worker("worker.js"),
status: "Idle"
})
}
// 線程池狀態(tài)更新函數(shù)
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") {
// 該線程空閑,更新狀態(tài)為忙碌
this.worker[i].status = "Busy"
// 更新線程池狀態(tài),如果這是最后一個空閑線程,那么線程池狀態(tài)變?yōu)闈M
this.statusUpdate()
// 返回給用戶該線程,和該線程的標識,標識用數(shù)組下標表示
return {
worker: this.worker[i].worker,
index: i
}
}
}
else {
// 此時線程池滿
let resolve = null
// 創(chuàng)建一個通知對象
let promise = new Promise(res => {
// 取得通知對象的狀態(tài)改變方法
resolve = res
})
// 通知對象的狀態(tài)改變方法加入阻塞請求隊列
this.blockRequestQueue.push(resolve)
// 返回給請求者線程池已滿信息和通知對象
return {
info: "full",
wait: promise
}
}
}
// 線程釋放方法,接收一個參數(shù)為線程標識
release(index) {
this.worker[index].status = "Idle"
// 阻塞請求隊列中的第一個請求出隊,隊列中存儲的是promise的resolve方法,此時執(zhí)行,通知請求者已經(jīng)有可用的線程了
if(this.blockRequestQueue.length)
// 阻塞請求隊列隊首出列,并執(zhí)行通知對象的狀態(tài)改變方法
this.blockRequestQueue.shift()()
// 更新線程池狀態(tài),此時一定空閑
this.status = "Idle"
}
}
5.spark-md5對文件進行md5編碼
說明:
在3的測試中spark-md5只是對簡單字符串進行MD5編碼,并非需要大量運算的耗時操作。spark-md5可以對文件進行MD5編碼,耗時較多,實現(xiàn)如下。
注意:
spark-md5對文件編碼時必須要對文件進行切片后再加密整合,否則不同文件可能會有相同編碼。詳情見github或npm。
// 在工作線程中引入spark-md5
self.importScripts("spark-md5.js")
let fd = new FileReader()
let spark = new self.SparkMD5.ArrayBuffer()
// 接收主線程發(fā)來的消息,是一個文件
self.onmessage = function(event) {
// 獲取文件
let chunk = event.data
// spark-md5要求計算文件的MD5必須切片計算
let chunks = fileSlice(chunk)
// 計算MD5編碼
load(chunks)
}
// 切片函數(shù)
function fileSlice(file) {
let pos = 0
let chunks = []
// 將文件平均切成10分計算MD5
const SLICE_SIZE = Math.ceil(file.size / 10)
while(pos < file.size) {
// slice可以自動處理第二個參數(shù)越界
chunks.push(file.slice(pos, pos + SLICE_SIZE))
pos += SLICE_SIZE
}
return chunks
}
// MD5計算函數(shù)
async function load(chunks) {
for(let i = 0; i < chunks.length; i ++) {
fd.readAsArrayBuffer(chunks[i])
// 在這里希望節(jié)約空間,因此復用了FileReader,而不是每次循環(huán)新創(chuàng)建一個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加密并使用線程池優(yōu)化
下面的測試代碼就是對上文所述的拼接
網(wǎng)頁代碼
<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.blockRequestQueue.push(resolve)
return {
info: "full",
wait: promise
}
}
}
release(index) {
this.worker[index].status = "Idle"
// 阻塞請求隊列中的第一個請求出隊,隊列中存儲的是promise的resolve方法,此時執(zhí)行,通知請求者已經(jīng)有可用的線程了
if(this.blockRequestQueue.length)
this.blockRequestQueue.shift()()
this.status = "Idle"
}
}
// input點擊事件處理函數(shù)
function handleChanged() {
let files = event.target.files
// 創(chuàng)建一個大小為2的MD5計算線程池
let pool = new MD5Pool(2)
// 計算切片文件的MD5編碼
Array.prototype.forEach.call(files, file => {
getMD5(file, pool)
})
}
// 獲取文件的MD5編碼的函數(shù),第一個參數(shù)是文件,第二個參數(shù)是MD5線程池
async function getMD5(chunk, pool) {
let thread = pool.assign()
// 如果info為full,那么說明線程池線程已被全部占用,需要等待
if(thread.info === "full") {
// 獲取線程通知對象
let wait = thread.wait
// 等到wait兌現(xiàn)時說明已經(jīng)有可用的線程了
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.onmessage = function(event) {
// 獲取文件
let chunk = event.data
// spark-md5要求計算文件的MD5必須切片計算
let chunks = fileSlice(chunk)
// 計算MD5編碼
load(chunks)
}
// 切片函數(shù)
function fileSlice(file) {
let pos = 0
let chunks = []
// 將文件平均切成10分計算MD5
const SLICE_SIZE = Math.ceil(file.size / 10)
while(pos < file.size) {
// slice可以自動處理第二個參數(shù)越界
chunks.push(file.slice(pos, pos + SLICE_SIZE))
pos += SLICE_SIZE
}
return chunks
}
// MD5計算函數(shù)
async function load(chunks) {
for(let i = 0; i < chunks.length; i ++) {
fd.readAsArrayBuffer(chunks[i])
// 在這里希望節(jié)約空間,因此復用了FileReader,而不是每次循環(huán)新創(chuàng)建一個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編碼,結(jié)果如下

總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
axios params 和 data 的區(qū)別小結(jié)
本文介紹了在前端開發(fā)中使用Axios發(fā)送HTTP請求時,params和data的區(qū)別及其應用場景,具有一定的參考價值,感興趣的可以了解一下2025-03-03

