2008年7月4日 星期五

Threading in Firefox 3

Firefox 3 (Gecko 1.9) 新增了許多特色 (詳見 ), 其中一項重要的特色, 便是導入了 Thread Manager 機制.

##CONTINUE##

在 Firefox 3 以前, 我們要讓 Javascript 程式在背景執行, 最簡單的方式便是利用 setTimeout / setInterval 方式:


function backgroundTask() {
// Perform a small amount of work

window.setTimeout(arguments.callee, 100);
}

window.setTimeout(backgroundTask, 100);

以上的方式雖然可以把程式丟到背景執行, 然而, 它依然只是在主執行緒(mainThread)中, 以非同步的方式執行.

Firefox 3 後, 導入了 Thread Manager , 讓我們可以撰寫平台相關的真正多執行緒程式.

要實作多執行緒 Javascript 程式,本身必須是 XPCOM Object, 且實作 nsIRunnable 介面(和 Java Thread 很像不是嗎?).
詳細範例可參考 Thread Manager nsIThreadManager .

Thread Utils:
雖然 Firefox 3 已經大大簡化了多執行緒程式的寫作方式(且很有 Java 風格), 但就 Javascript 來說, 就顯的有些麻煩, 我們要為每一個要執行的程式, 一一實作成 XPCOM Object 也是一件很煩人的事..
阿土伯在 GREUtils Rewriting 計劃中, 為 Threading 做了一些封裝, 讓您可以比較容易且直覺的使用 Firefox 3 所帶來的 Multi-Threading 這份大禮.

有關 Thread 部份的 Code 如下, 已修改過可以獨立運作於 GREUtils 整個專案之外:

/**
* Thread - is simple and easy use Thread Manager APIs libraries for GRE (Gecko Runtime Environment).
* ONLY Work with Firefox 3 or XULRunner 1.9
*
* @public
* @name GREUtils.Thread
* @namespace GREUtils.Thread
*/
var GREUtils = GREUtils || {};

GREUtils.Thread = {

_threadManager: Components.classes["@mozilla.org/thread-manager;1"]
.getService(Components.interfaces.nsIThreadManager),

_mainThread: null,

_workerThread: null,

reportError: function(err){
Components.utils.reportError(err);
}
};


/**
* getThreadManager
*
* @public
* @static
* @function
* @return {Object} nsIThreadManager
*/
GREUtils.Thread.getThreadManager = function(){
return this._threadManager;
};


/**
* getMainThread
*
* @public
* @static
* @function
* @return {Object} nsIThread main thread
*/
GREUtils.Thread.getMainThread = function(){
if (this._mainThread == null) {
this._mainThread = GREUtils.Thread.getThreadManager().mainThread;

// extends magical method to worker thread
// this._workerThread.dispatchMainThread = GREUtils.Thread.dispatchMainThread;
}
return this._mainThread;
};


/**
* dispatchMainThread
*
* @public
* @static
* @function
* @return {Object} nsIThreadManager
* @param {Object} aRunnable
* @param {Object} aType
*/
GREUtils.Thread.dispatchMainThread = function(aRunnable, aType) {
var mainThread = GREUtils.Thread.getMainThread();
var aType = aType || mainThread.DISPATCH_NORMAL;
try {
mainThread.dispatch(aRunnable, aType);
}catch (err) {
GREUtils.Thread.reportError(err);
}
};


/**
* dispatchWorkerThread
*
* @public
* @static
* @function
* @return {Object} nsIThreadManager
* @param {Object} workerThread
* @param {Object} aRunnable
* @param {Object} aType
*/
GREUtils.Thread.dispatchWorkerThread = function(workerThread, aRunnable, aType) {
var aType = aType || workerThread.DISPATCH_NORMAL;
try {
workerThread.dispatch(aRunnable, aType);
}catch (err) {
GREUtils.Thread.reportError(err);
}
};

/**
* getWorkerThread in pool
*
* @public
* @static
* @function
* @return {Object} nsIThread worker thread
*/
GREUtils.Thread.getWorkerThread = function(){
// get presist work thread
// will not create new worker thread
if (this._workerThread == null) {
this._workerThread = GREUtils.Thread.getThreadManager().newThread(0);

// extends magical method to worker thread
// this._workerThread.dispatchMainThread = GREUtils.Thread.dispatchMainThread;
}
return this._workerThread;
};


/**
* createWorkerThread - create new worker thread.
*
* @public
* @static
* @function
* @return {Object} nsIThread worker thread
*/
GREUtils.Thread.createWorkerThread = function(){
// create new worker thread
var worker = GREUtils.Thread.getThreadManager().newThread(0);

// extends magical method to worker thread
//worker.dispatchMainThread = GREUtils.Thread.dispatchMainThread;

return worker;
};


/**
* CallbackRunnableAdapter
*
* @public
* @class
* @param {Object} func
* @param {Object} data
*/
GREUtils.Thread.CallbackRunnableAdapter = function(func, data) {
this._func = func;
this._data = data;
};

GREUtils.Thread.CallbackRunnableAdapter.prototype = {

get func() {
return this._func;
},

set func(func){
this._func = func || null;
},

get data() {
return this._data;
},

set data(data){
this._data = data || null;
},

run: function() {
try {
if (this.func) {
if(this.data) this.func(this.data);
else this.func();
}
} catch (err) {
Components.utils.reportError(err);
}
},

QueryInterface: function(iid) {
if (iid.equals(Components.Interfaces.nsIRunnable) || iid.equals(Components.Interfaces.nsISupports)) {
return this;
}
throw Components.results.NS_ERROR_NO_INTERFACE;
}
};


/**
* WorkerRunnableAdapter
*
* @public
* @class
* @param {Object} func
* @param {Object} callback
* @param {Object} data
*/
GREUtils.Thread.WorkerRunnableAdapter = function(func, callback, data) {
this._func = func;
this._callback = callback;
this._data = data;

if(arguments.length == 2 ) {
this._data = callback;
this._callback = null;
}
};

GREUtils.Thread.WorkerRunnableAdapter.prototype = {

get func() {
return this._func;
},

set func(func){
this._func = func || null;
},

get callback() {
return this._callback;
},

set callback(callback){
this._callback = callback || null;
},

get data() {
return this._data;
},

set data(data){
this._data = data || null;
},

run: function() {
try {
var result = null;
if (this.func) {
if(this.data) result = this.func(this.data);
else result = this.func();
}

if (this.callback) {
GREUtils.Thread.dispatchMainThread(new GREUtils.Thread.CallbackRunnableAdapter(this.callback, result));
}
} catch (err) {
Components.utils.reportError(err);
}

},

QueryInterface: function(iid) {
if (iid.equals(Components.Interfaces.nsIRunnable) || iid.equals(Components.Interfaces.nsISupports)) {
return this;
}
throw Components.results.NS_ERROR_NO_INTERFACE;
}
};


/**
* createWorkerThreadAdapter
*
* @public
* @static
* @function
* @param {Object} workerFunc
* @param {Object} callbackFunc
* @param {Object} data
*/
GREUtils.Thread.createWorkerThreadAdapter = function(workerFunc, callbackFunc, data) {

return new GREUtils.Thread.WorkerRunnableAdapter(workerFunc, callbackFunc, data);
};


使用方式:
利用 GREUtils.Thread.getMainThread() 可以取得主執行緒.
利用 GREUtils.Thread.createWorkerThread() 產生一個新的執行緒.

WorkerThreadAdapter:
則是用以簡化自行撰寫 worker / callback 二個 XPCOM Object, 取而代之的是二個 Function handle 以及要傳入 worker thread 的資料.


var worker = GREUtils.Thread.createWorkerThread();
var execute = GREUtils.Thread.createWorkerThreadAdapter(
/* worker function */
function(data) {
var result = 0;
var i = 0;

while(i <= data) {
result += i;
i++;
}
return result;
},
/* callback function */
function(result) {
alert('sum = ' + result);
},
/* data to worker function */
10000
);

GREUtils.Thread.dispatchWorkerThread(worker, execute);



註1:
Firefox3 的 Thread 由於是以 XPCOM 實作, 所以您只能用於 extension addons 寫作或是 XULRunner 專案中, 如果您需要在一般 Web Application 中使用多執行緒, 可以參考 Google Gears 的 WorkerPool 實作.

註2:
請特別注意 The Thread Manager 那篇的這句重要的警告.
Note: The DOM is not thread safe. You must not access the DOM or user interface from background threads. Doing so will likely crash.
其中要再注意的是, alert() 也是 DOM global scope 哦, 如果您在 background threads 用了 alert(), Firefox 3 也是會當給你看地.


忙裡偷閒寫下本篇, 內容有點雜亂簡單, 但還是希望對大家有幫助.