客戶端JavaScript的線程池設計詳解
1.介紹:
本打算在客戶端JavaScript進行機器學習算法計算時應用線程池來優化,就像()演示的神經網絡。但是由於各種原因不瞭瞭之瞭。本次遇到瞭一個新的問題,客戶端的MD5運算也是耗時操作,如果同時對多個字符串或文件進行MD5加密就可以使用線程池來優化。
2.準備工作:
到npm官網搜索spark-md5,到其github倉庫下載spark-md5.js。該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計算函數 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() } }) } }
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.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" } } // 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.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計算函數 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編碼,結果如下
總結
本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!
推薦閱讀:
- Vue+NodeJS實現大文件上傳的示例代碼
- JavaScript實現大文件分片上傳處理
- 如何基於js管理大文件上傳及斷點續傳詳析
- 徹底搞懂 javascript的Promise
- Javascript如何理解全國甲卷高考作文