[Firefox 3.5 preview] web worker thread

 Fri, 26 Jun 2009 21:13:39 +0800

Firefox 3.5 RC剛剛出來,在他的 Release Note 裡面提到了一些新的功能,包含native JSON support以及web worker thread這兩個新的javascript功能。(另外,透過安裝addon,還可以支援canvas 3d)

如果熟悉Javascript,應該就知道他是single thread的。現在透過web worker,可以讓javascript擁有一些真正多工的能力(根據說明,他會使用到系統層級的thread)。

雖然web worker使用上有一些限制,例如每次要新增一個worker,都可能會有網路的loading,不過適當地使用還是對效率有所幫助。

使用上有幾個地方要先注意:

  1. worker執行的環境是一個完全獨立的context,這個context裡面的Global物件幾乎沒什麼東西,只包含了onmessage事件屬性用來指定事件處理函數,以及postMessage用來回傳資料並觸發worker物件的onmessage事件。
  2. worker物件有同樣有onmessage屬性,指定給他的事件處理函數會在worker載入的Javascript執行中呼叫postMessage時被觸發,而postMessage方法則可以觸發載入的Javascript中的onmessage。
  3. 根據Mozilla的文件說明,透過postMessage傳遞的資料,會先轉成JSON再轉回成物件,並不是直接把一個物件的參考傳入,所以針對接收到的物件所作的修改,對傳入的物件完全不會有作用。
  4. 簡單地說,在worker所載入執行的Javascript裡面,完全無法操控DOM,能做的只有接收傳進來的資料並傳回處理過的結果。

接下來就試用一下吧。先不做困難的東西,只簡單地比較一些使用worker的方法及效率。程式碼如下:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<title>test486.html</title>
<body>
<div id="panel"></div>
<script type="text/javascript">
var workers = [];
var str = "";
var count = 0;
var start = new Date().getTime();
var total = 0;
for(var i=0; i<4; i++) {
    workers[i] = new Worker('test486.js');
    workers[i].onmessage = function(event) {
        str += event.data +"<br>";
        count++;
        if(count == 100) {
            total = new Date().getTime()-start;
            document.getElementById('panel').innerHTML = str;
            alert(total/1000);
            for (var x=0; x<4; x++) {
                workers[x].terminate();
                workers[x] = null;
            }
            workers = null;
            count = 0;
            str = "";
            test2();
        }
    }
}
for (var i=0; i<25; i++) {
    for(var j=0; j<4; j++) {
        workers[j].postMessage([j,new Date().getTime()]);
    }
}
function test2() {
    start = new Date().getTime();
    workers = [];
    for (var i=0; i<100; i++) {
        workers[i] = new Worker('test486.js');
        workers[i].onmessage = function(event) {
            str += event.data +"<br>";
            count++;
            if(count == 100) {
                total = new Date().getTime()-start;
                document.getElementById('panel').innerHTML = str;
                alert(total/1000);
                for (var j=0; j<100; j++) {
                    workers[j].terminate();
                    workers[j] = null;
                }
                workers = null;
            }
        }
        workers[i].postMessage([i, new Date().getTime()]);
    }
}
</script>
</body>
</html>

載入的test486.js內容如下:

var result = [];
 
onmessage = function(event) {
    result.push(event.data[1]);
    postMessage(event.data[0]+":"+result);
};

接下來看一下結果:

第一個部份是用四個web worker模仿thread pool,用這四個worker來進行一百個task。

另一個是產生一百個web worker來進行一百個task:

速度慢了不少,可以看出每一個worker都有一些overhead,所以使用時要斟酌。

另外,在test486.js中的全域變數並不會因為onmessage事件執行完畢就消失,worker只是在一個thread中等待事件發生然後執行處理函數,每次執行都會影響相關的變數。

另外,不論是在worker或是網頁中,事件處理函數的執行都是用一個佇列在管理,這樣就不會有racing發生,所以不用特別lock住相關的變數,系統一定會根據佇列中執行的順序去更動變數,但是事件可能不會依照預期的順序發生就是了。

另外試作複雜一點的例子,用worker來做partial reduce:

<html>
<title>test487.html</title>
<body>
<div id="panel"></div>
<script>
var str = "";
var test = [];
for(var i=0; i<10000; i++) {
    if(i<5000) {
        test[i] = 0.01;
    } else {
        test[i] = 0.02;
    }
}
var workers = [];
var start = new Date().getTime();
var status = [];
var poolsize = 6;
for (var i=0; i<poolsize; i++) {
    workers[i] = new Worker('test487.js');
    workers[i].onmessage = function(event) {
        test.unshift(event.data);
        str += event.data + "<br>";
        if (test.length<2) {
            status.push(1);
            if (status.length == poolsize) {
                alert((new Date().getTime()-start)/1000);
                alert(test);
                document.getElementById("panel").innerHTML = str;
                for (var x=0; x<poolsize; x++) {
                    workers[x].terminate();
                }
            }
        } else {
            this.postMessage([test.pop(), test.pop()]);
        }
    }
    workers[i].postMessage([test.pop(), test.pop()]);
}
</script>
</body>
</html>

載入的test487.js內容如下:

onmessage = function(event) {
    postMessage((event.data[0] + event.data[1]));
}

其實這樣子做計算,效率很不好啦,速度很慢:

計算過程:

web worker的global object裡面同樣有XMLHttpRequest物件可以使用,用他在背景取得ajax內容完全不會影響頁面程式的執行,這也許是最有用的功能吧?Mozilla網站上有一些簡要的說明,可以參考一下:Using web workers,裡面還有一些連結可以當作進一步的參考。(包括HTML5裡面的worker規格文件,web worker只是worker的一種,不過目前Firefox 3.5也只支援web worker)