對webWorker多執行緒效能的初步探索

藥不能停呀發表於2017-11-08

前言:一直都覺得只有java才能談多執行緒這麼高逼格的話題,可惜java和javascript的關係就好比雷鋒跟雷峰塔的關係,不過在HTML5新特性中有一個webworker,關於它的理論如果沒有相關demo去實踐它可能理解起來會有點僵硬。本文是通過canvas裡面的一個操作畫素的demo來更為直觀地展現webworker的效能。

js的單執行緒特性

JavaScript引擎是單執行緒執行的,JavaScript中耗時的I/O操作都被處理為非同步操作,它們包括鍵盤、滑鼠I/O輸入輸出事件、視窗大小的resize事件、定時器(setTimeout、setInterval)事件、Ajax請求網路I/O回撥等。當這些非同步任務發生的時候,它們將會被放入瀏覽器的事件任務佇列中去,等到JavaScript執行時執行執行緒空閒時候才會按照佇列先進先出的原則被一一執行,但終究還是單執行緒。只要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行。常見的瀏覽器無響應(假死),往往就是因為某一段Javascript程式碼長時間執行(比如執行一個運算量非常大的函式),導致整個頁面卡在這個地方,其他任務無法執行。

非同步和單執行緒

正是因為單執行緒所以註定了js中的某些操作必須是非同步的,舉個粒子:

window.onload = function () {
            console.log(111);
            setTimeout(function() {
                console.log(222);
            }, 0);
            console.log(333);            
        }複製程式碼

上面的程式碼依次輸出111,333,222,因為執行js碰到屬於非同步操作的程式碼就會單獨拎出來放在一個佇列(setTimeout就屬於非同步操作,不管是不是0秒後執行),等待同步程式碼(不屬於非同步操作的程式碼就是同步了)執行完就依次執行佇列裡的操作。其實在head裡面載入js的話window.onload本身也屬於非同步程式碼。

實現非同步操作的方法

1、利用setTimout或回撥實現非同步

2、動態建立script標籤

3、利用script提供的defer/async

4、es6裡的promise

這裡只是總結一下,覺得前三種都很雞肋,用好回撥和promise大法就好了。

什麼是webworker

Web Worker 是HTML5標準的一部分,這一規範定義了一套API,它允許一段JavaScript程式執行在主執行緒之外的另外一個執行緒中。工作執行緒允許開發人員編寫能夠長時間執行而不被使用者所中斷的後臺程式, 去執行事務或者邏輯,並同時保證頁面對使用者的及時響應,可以將一些大量計算的程式碼交給web worker執行而不凍結使用者介面。這是這個標題幾乎標準的答案,好像還是可以理解的。

webworker的基本語法

//---------------放在頁面上的main.js:主執行緒
var worker = new Worker("worker.js");
// 主執行緒向子執行緒傳送資料
worker.postMessage(data);
worker.onmessage = function(ev){
    var data = ev.data;
    // 主執行緒收到子執行緒的處理後的資料
};
//-----------------------------------//

//-------------放在後臺的worker.js:子執行緒
self.onmessage = function(ev){
   var data = ev.data //收到主執行緒發過來的源資料
   var newdata = handle(data);
   self.postMessage(newdata );//把處理好的資料發回去
};
//把一些運算不叫複雜或工作量大的js程式碼拎過來
function handle(data){ some coding... }
//----------------------------------//
//-----------------------------停止worker
// 方式一 main.js 在主執行緒停止方式 
var worker = new Worker('./worker.js');
...
worker.terminate();

// 方式二、worker.js
self.close();複製程式碼

這裡就先羅列一個最基本的流程語法吧,非常簡單,需要特別注意:子執行緒裡面的js程式碼所支援的語法非常有限,只支援ECMAscript的基本語法,具體範圍是多大呢,我們都知道javascript大致分為ECMAscript、DOM、BOM、nodeJs。第一類是基礎,DOM類是基於ECMAscript實現的去操作DOM樹的,BOM類就是瀏覽器行為語法,而nodeJs則是基於ECMAscript去操作os、file、database、net等等之類的(不知道這樣理解會不會被人打^_^)。所以它只支援像string、array、object...這樣子的東西,連console.log和alert都不支援,相信大家肯定知道這個範圍。還有上面說了執行緒是後臺開的,這些檔案自然要放在伺服器環境下執行的。在子執行緒中還可以通過 importScripts( 'another1.js' ,'another2.js' )來引入其他的js檔案,是不是看到一點原生js模組化的影子呢,然而這其實沒什麼軟用。

demo:用canvas語法操作十萬顆畫素點

//------------------------------------------------主執行緒
window.onload = function () {
    var oc = document.getElementById("print");
    var ogc = oc.getContext('2d');
    ogc.fillStyle = "#fc6423";
    var ali = document.getElementsByTagName('li');
    for (var i = 0; i < ali.length; i++) {
        ali[i].onclick = function () {
            console.time(1);//------------------計時開始
            ogc.clearRect(0, 0, oc.width, oc.height);
            ogc.save();
            var h = 200;
            var str = this.innerHTML;
            ogc.font = h * 0.85 + 'px impact';
            ogc.textBaseline = 'top';
            var w = ogc.measureText(str).width;
            ogc.fillText(str, (oc.width - w) / 2, (oc.height - h) / 2);
            var allarea = ogc.getImageData((oc.width - w) / 2, (oc.height - h) / 2, w, h);
            console.log(w);
            ogc.clearRect(0, 0, oc.width, oc.height);
            var newarea = ogc.createImageData(w, h);
            // var allarr = suiji(w * h, w * h / 10);
//這裡開一個worker,為的就是把上一行程式碼:10組隨機數的產生拎到子執行緒中去執行
            var worker = new Worker('cutarr.js');
            worker.postMessage(w*h);// 將資料發給子執行緒
            worker.onmessage = function (ev) {
                var allarr = ev.data;//收到處理後的資料                        
                var inow = 0;
                var timer = null;
                timer = setInterval(function () {
                    for (var i = 0; i < allarr[inow].length; i++) {
          newarea.data[allarr[inow][i] * 4] = allarea.data[allarr[inow][i] * 4];
          newarea.data[allarr[inow][i] * 4 + 1] = allarea.data[allarr[inow][i] * 4 + 1];
          newarea.data[allarr[inow][i] * 4 + 2] = allarea.data[allarr[inow][i] * 4 + 2];
          newarea.data[allarr[inow][i] * 4 + 3] = allarea.data[allarr[inow][i] * 4 + 3];
                    }
                    ogc.putImageData(newarea, (oc.width - w) / 2, (oc.height - h) / 2);
                    if (inow == 9) {
                        inow = 0;
                        clearInterval(timer);
                    }
                    inow++;
                }, 150)
                ogc.restore();                  
            }
            console.timeEnd(1); //------------------計時結束
        }
    }      
}
//--------------------------------------------------子執行緒
function suiji(all, part) {
    var arr1 = [];
    var allarr = [];
    for (var i = 0; i < all; i++) {
        arr1.push(i);
    }
    for (var i = 0; i < all / part; i++) {
        var newarr = [];
        for (var j = 0; j < part; j++) {
            newarr.push(arr1.splice(Math.floor(Math.random() * arr1.length), 1));
        }
        allarr.push(newarr);
    }
    return allarr;
}
self.onmessage = function (ev) {
    var arr = suiji(ev.data,ev.data/10);
    self.postMessage(arr);
}複製程式碼

上面這段js程式碼要完成的是將一塊區域內的近10萬顆畫素(可調文字大小改變)每隔150ms隨機顯示其1/10的畫素點,最終形成一個完整的漢字,其中最耗時的過程在於隨機數的產生,有幾個畫素點就進行幾次random操作。這是由隨機函式suiji來完成,這裡單純地把它拎到子執行緒中去處理。上面的程式碼是用了webworker的,不使用webworker:即把suiji()放在window.onload裡面,直接用 var allarr = suiji(w h, w h / 10)來產生就好了。接下來對比一下兩種請況下從點選每個字到列印計時一共花費多少時間:

不使用worker
不使用worker

使用worker
使用worker

一個接近2秒,一個10ms以內,這。。。而且前面提到:工作執行緒允許開發人員編寫能夠長時間執行而不被使用者所中斷的後臺程式, 去執行事務或者邏輯,並同時保證頁面對使用者的及時響應,也就是說,當瀏覽器碰到這句程式碼:

 var allarr = suiji(w * h, w * h / 10);複製程式碼

頁面就會卡死近2000ms,不能有任何操作,比如說點選、右鍵,更不用說執行後面的程式碼了:

    console.log(111);       
    var allarr = suiji( w*h,w*h/10 );
    console.log(222);複製程式碼

這樣我看到:幾乎在點選的同時就列印了111,而222、計時和分割線是在近2000ms之後同時列印的,因為上面三行程式碼都是同步的,而用了webworker則不存在卡死的請況,速度也快多了。如果面積小看不出有多少差別,可以如果把大小調到500,前者就達到了恐怖的15秒之多了。可是這時候用了webworker雖然不會造成卡死狀態,但好像也快不了多少,也許在一個可控的範圍內才能發揮它的效能吧,或者說本來js程式碼對多執行緒的支援就不好。大家可以自己去編寫更復雜變態的函式去測驗。

案例原始碼地址:github.com/formattedzz…

部分典型的應用場景

Web Worker帶來後臺計算能力,WebWorker自身是由webkit多執行緒實現,但它並沒有為Javasctipt語言帶來多執行緒程式設計特性,我們現在仍然不能在Javascript程式碼中建立並管理一個執行緒,或者主動控制執行緒間的同步與鎖等特性。Web Worker 只是瀏覽器(宿主環境)提供的一個能力/API。而且它不支援IE。

1、使用專用執行緒進行數學運算
Web Worker最簡單的應用就是用來做後臺計算,而這種計算並不會中斷前臺使用者的操作

2、 影象處理
通過使用從canvas或者video元素中獲取的資料,可以把影象分割成幾個不同的區域並且把它們推送給並行的不同Workers來做計算

3、大量資料的檢索
當需要在呼叫 ajax後處理大量的資料,如果處理這些資料所需的時間長短非常重要,可以在Web Worker中來做這些,避免凍結UI執行緒。

4、背景資料分析
由於在使用Web Worker的時候,我們有更多潛在的CPU可用時間,我們現在可以考慮一下JavaScript中的新應用場景。我們現在可以考慮一下JavaScript中的新應用場景。例如,我們可以想像在不影響UI體驗的情況下實時處理使用者輸入。利用這樣一種可能,我們可以想像一個像Word(Office Web Apps 套裝)一樣的應用:當使用者打字時後臺在詞典中進行查詢,幫助使用者自動糾錯等等。

參考文章:www.alloyteam.com/2015/11/dee…

相關文章