使用 XUL 除了可以創建跨平台的桌面應用程序之外,還可以擴展您使用 Javascript、CSS 甚至 Html 的技能。XUL 跨平台功能絕非簡單的特性集合。相反,XUL 為您提供了與桌面應用程序工具箱類似的功能,包括對本地線程的訪問。您甚至可以通過編寫能平行執行的代碼來直接從 JavaScript 訪問本地線程。在本文中,您將了解 XUL 的多線程功能,並創建一個應用程序,用來通過多線程檢索數據。您將使用的是一個能訪問 Internet 上的多個遠端數據源的典型 IO-綁定應用程序,您將通過 XUL 內的多線程來加速這個應用程序。此應用程序允許用戶來查看和對比來自三個流行的搜索引擎 Google、 Yahoo 以及 Microsoft ® 的匿名結果。
前提條件
在本文中,您將開發一個 XUL(XML User Interface Language)應用程序。熟悉 XUL 開發會有幫助,但並非硬性要求。XUL 更像是一個 XML 小部件語言,在某些方面與 Html 相似,也支持 Javascript 和 CSS。您可以將其他語言與 XUL 結合使用,但在本文中我們只使用 JavaScript。因此,需要您具有 Javascript 的經驗,但對其他語言則不做要求。本文中使用的這些並發性 API 已經被引入了 Mozilla 的 JavaScript 1.9.1,它是 Firefox 3.5 的一部分。因而,您可以使用 Firefox 3.5 或 Mozilla 的 XULRunner 1.9.1 來運行本文中的代碼。
常用縮略語
API:應用程序編程接口(Application program interface)
CPU:中央處理器(Central Processing Unit)
CSS:級聯樣式表(Cascading Stylesheets)
Html:超文本標記語言(Hypertext Markup Language)
IO:輸入-輸出(Input-Output)
JSON:JavaScript Object Notation
UI:用戶界面(User Interface)
URL:統一資源定位符(Uniform Resource Locator)
XML:可擴展標記語言(Extensible Markup Language)
XPCOM:跨平台組件對象模型(Cross Platform Component Object Model)
XUL:XML 用戶界面語言(XML User Interface Language)
問題語句
早在 2005 年,Herb Sutter 就說過免費午餐的時代已經結束了。他的分析和結論都非常簡單。摩爾定律已經完成了其使命。計算機的先進不再只體現在處理器的速度上。軟件開發人員也已經很好地利用了不斷增長的處理速度,他們總是在不斷增加所需要的計算量!這是顯而易見的。計算機的運行速度加快意味著您可以做更多的事情。現在,主要通過添加內核來使計算機速度變得更快。您雖然還是可以做更多的事情,但必須使用並發編程才行。您需要使用這些內核。
也許您對此早就有所耳聞。很多編程語言和平台都已增加了新的並發性功能以幫助開發人員充分利用多內核的計算機。對於某些已經具有多線程編程 API 的語言,這就意味著通過創建更好的 API 和抽象可以使多線程編程更易懂、更少出錯。一些語言本身並不支持多線程編程,對多線程編程的支持是後來加上的。XUL 程序員最主要的編程語言 JavaScript 就是這樣的例子。從 Javascript 1.9.1 開始,多線程編程就被加入了 XUL 平台上的 JavaScript。
ye olde XUL 中的線程
現在,您可能會對自已說:“等等! XUL 是 Firefox 及其擴展背後的主要技術。不要告訴我這些 XUL 應用程序一直以來都是單線程的!” 您說的當然對。但關鍵是要記住 XUL 支持的不只是 JavaScript。借助 XPCOM,還可以通過其他語言實現組件,特別是通過 C++。當然,C++ 能支持本機線程,所以總是可以通過該種方式在一個 XUL 應用程序中使用多線程。不過,XUL 的本機編程語言通常總是 JavaScript,或更確切地說是 Mozilla 式的 JavaScript。直到現在,JavaScript 仍然缺少一種能顯式派生和管理任意線程的方法,但 Javascript 1.9.1 卻改變了這種現狀。盡管只是一個較小的發布版,JavaScript 1.9.1 卻增加了一項重要的新特性:通過 Workers API 實現多線程。
Workers API
那麼 Javascript 中的這個 Workers API 究竟是什麼呢?有了 Workers,就可以隨意派生線程,並且可以使用這些線程實現任何想要完成的任務。在很多情況下,您都會希望在應用程序中派生一個或多個線程。一種常見用例是執行一些周期性操作。這在很多應用程序中十分常見,開發人員會使用 JavaScript 的 setInterval 函數來實現這個操作。此函數的惟一缺陷是:它在主線程上執行,因此,在此期間,它會阻塞與用戶的任何交互。如果它的執行占用了顯著的時間,那麼在執行期間應用程序就會凍結。所以,最好在一個獨立的線程上執行這個函數。
這也讓我想起了多線程的一個更常用的用例:長時間運行的操作。任何在主線程上運行的這類操作都會導致應用程序凍結。我們總是希望這類操作在其自已的線程上執行。那麼長時間運行的操作有哪些例子呢?您可能見到過大整數質因數分解,或是對復雜數據結構的龐大列表進行分類,這些都是長時間運行的操作。但它們並不是 XUL 應用程序中的常見任務。XUL 應用程序中較為常見的是一些 IO 類的操作,如訪問 Web 服務或是從本地文件系統讀取。這也是我們要在本文中研究的一種操作。您將看到 Workers API 不僅允許派生線程,而且還允許在所派生的線程上任意地執行復雜的任務。最後,作為這類操作的結果,可能需要對 UI 執行某些更新,但是用另外一個線程實現這個目的是很有風險的。幸好 Workers API 在設計時考慮到了這一點,極大地簡化了原本可能很困難的場景。下面讓我們看看如何利用 Workers API,並在一個簡單的應用程序的上下文中對它進行研究。
口味測試應用程序
如前面所提到的,多線程的一個常見應用就是某類 IO 操作,例如訪問 Internet 上的一個 Web 服務。現在 JavaScript/XUL 總是可以為網絡操作提供多線程:XMLHttpRequest。AJax 的秘密武器就是在後台線程上執行網絡 IO,而調用程序則提供一個 callback 函數,該函數在 IO 完成後在主線程上調用。不過,您對這個線程沒有顯式的控制;運行時負責這一點。此外,如果需要做多個網絡操作,又該怎麼辦呢?在這種情況下,請求將被運行時序列化。在本應用程序中,我們將需要執行這一類任務 — 通過 Workers API 用多個線程同時訪問多個遠程 Web 服務。當用戶查看並比較 Google、Yahoo 或 Microsoft Bing 這三個搜索引擎的匿名結果時,此應用程序將允許用戶執行一個 “蒙目(blind taste)的口味測試”。
三個搜索引擎,三個線程
下面將要探討的這個應用程序非常簡單。此應用程序允許用戶輸入一個或多個搜索條件。隨後它將搜索這三個搜索引擎,並且匿名地顯示搜索結果,因此用戶並不知道哪個結果列表出自於哪個搜索引擎。我們從用戶界面的 XUL 代碼開始(參見 清單 1)。
清單 1. XUL UI 代碼<hbox>
<label value="Enter" id="lbl"/>
<textbox value="" id="term"/>
<button label="Go!" onclick="search()" id="btn"/>
</hbox>
這是一段簡單的 XUL 代碼,可創建一個搜索表單。它用三個小部件創建了一個水平布局。第一個小部件是一個標簽,第二個小部件是用戶可在其中輸入搜索條件的一個文本框,第三個小部件是一個按鈕。當用戶單擊這個按鈕時,就會調用搜索函數。這也是派生 worker 線程的地方,如 清單 2 所示。
清單 2. 派生 Workersfunction search(){
var keyWord = document.getElementById("term").value;
var workerScripts = ["google.js", "yahoo.js", "bing.JS"];
var worker = {};
for (var i=0;i<workerScripts.length;i++){
worker = new Worker(workerScripts[i]);
worker.onmessage = function(event) {
displayResults(event.data);
};
worker.postMessage(keyWord);
}
}
如果您使用的是基於 JavaScript 1.9.1 以前版本的 XUL,那麼就需要對所有這三個請求進行排隊,或是使用 XPCOM 與 C++ 來有效地檢索這些數據。有了 Javascript Workers,派生多線程來檢索該數據就變得十分簡單。每個 Worker 對象都為其構造函數獲取了一個單一字符串。這個字符串是另一個 JavaScript 文件的所在位置,該文件將成為這個 worker 的主體。worker 文件中的腳本將在其自已的線程上執行。
所談論的這三個腳本被分別命名為 google.js、yahoo.js 和 bing.JS。將它們放入一個數組,然後對該數組進行迭代。對於這個數組中的每個腳本,只需傳遞進指定 worker 腳本位置的那個字符串就可以創建 Worker。接下來,針對每個所創建的 worker,設置其 onmessage 函數。這個函數將在 worker 向主線程返回數據時被調用。在本例中,將此工作簡單地委托給另一個名為 displayResults 的函數就可以了。最後,要發送想要用於各搜索引擎的搜索條件的名稱,需要在每個 Worker 對象上調用 postMessages 函數。這將使此 worker 線程得以訪問 Web 服務,進而,就可以並發調用這三個 Web 服務。不僅調用是同時的,而且結果的處理也是並行完成的。
那麼這個 onmessage/postMessage 范型到底說明了什麼?這是 Workers API 背後的基礎線程模型。為大多數開發人員所熟知的常用線程模型是被 C++、 Java™ 技術及其他編程語言所使用的那個模型。為了通信,線程通常會修改所共享的內存。這很有效,但會帶來很多問題(信號量、互斥等)。對於 UI 編程來說,也會遇到同樣的問題,因為如果用多個線程更改 UI,就可能會導致 UI 鎖死或崩潰。為了避免將所有這些復雜性帶入 XUL,JavaScript Workers API 使用了一個更簡單的模型。這個模型建立在消息傳遞的基礎上,並且與 Erlang 和 Scala 中的 actor 模型有點類似。
讓我們返回到 清單 2,Worker 實例充當所派生線程的一個代理。要向線程發送一個消息,需要調用 Worker 實例上的 postMessage 方法。任何對象都可以被傳遞到這個方法。當然,這個派生的線程也可以將消息傳遞回其母線程。在向母線程傳遞消息時,會調用 Worker 實例的 onmessage 方法。這是一個不可選的默認實現,因此必須要覆蓋它(如果您對從這個線程返回的結果感興趣的話)。在 清單 2 中,要覆蓋它,需要將它設置為等同於能接受單一參數的函數,名為 event。這是被從派生線程發送來的對象。對於 清單 2 中的這個函數,提取 event 的 data 屬性,因為這是被線程發送的實際數據,然後將它發送給另一個函數來更新 UI。
您已經對母線程有了一點了解。那麼對於派生或子線程您又了解了多少呢?如前面 清單 2 所示,對於每個派生線程,都要使用單獨一個 JavaScript 文件。在 清單 3 中,我們先來看看訪問 Google 搜索 API 的那個線程。
清單 3. Google 搜索 workeronmessage = function(event){
var keyWord = event.data;
var results = searchGoogle(keyWord);
postMessage(results);
}
function searchGoogle(keyWord){
var url = "http://ajax.googleapis.com/AJax/services/"+
"search/web?v=1.0&rsz=large&q="+keyWord;
var xhr = new XMLHttpRequest();
xhr.open("GET", this.url, false);
xhr.send();
var response = JSON.parse(xhr.responseText);
var results = [];
var result = {};
var data = response.results;
for (var i=0;i<data.length;i++){
result.url = data[i].url;
result.title = data[i].title;
result.description = data[i].content;
results.push(result);
}
return results;
}
清單 3 中的代碼與 清單 2 中的代碼有些相似。這個線程有一個 onmessage 方法。它就是每當消息被發送給線程時就會調用的函數。如果回到清單 2,那裡調用的是 Worker 實例上的 postMessage 方法。這將導致清單 3 中的 onmessage 的函數的調用。這樣一來,傳遞給 postMessage 的這個對象就成為了傳遞給 onmessage 方法的那個對象的數據屬性。它是本示例中搜索用的關鍵字。
回到 清單 3,一旦從傳遞來的消息中抽取出了關鍵字,就會調用 searchGoogle 函數。此函數使用 Google 的搜索 API 來請求針對這個關鍵字的搜索結果。要調用此 Web 服務,請注意使用標准的 JavaScript API XMLHttpRequest。您可能已經注意到了,在用 open 方法發送這個請求時,指定了三個參數。其中最後一個參數指定了此請求是否是異步的。因它默認為 true,所以此請求是異步的。在這種情況下,需要覆蓋 XMLHttpRequest 實例的 onreadystatechange 函數以便您可以為這個異步請求設置一個 callback 函數。然而,如果是從一個 Worker 線程做這樣的請求,就無需這麼做了。而只需將這個異步標志設置為 false,進行同步調用。這樣,send 方法將被一直阻塞,直到從 Web 服務返回響應。這讓您能很輕松地處理此響應。
在本例中,來自於 Google 的響應是一個 JSON 結構。您可以直接地 eval 這個響應,但使用解析器將會更安全。Javascript 1.9.1 中也包含了來自於 json.org 的標准 JSON 解析器。因此您只需使用它的解析方法就可以安全地將 JSON 數據解析為一個 JavaScript 對象。剩下的代碼則只需從 Google 返回的結構中提取數據並將這些數據放入一個公共結構。該數據隨後會被通過 postMessage 函數發送回母線程。當調用 worker 線程上的 postMessage 函數時,就會用從這個 worker 線程發送來的數據調用 Worker 實例(在母線程中)上的 onmessage 方法。這就是為什麼要設置 清單 2 中的這個 onmessage 函數的原因所在。其他的每個 Workers 都做著同樣的工作。例如,清單 4 顯示了作用於 Bing 搜索引擎的那個 worker 線程。
清單 4. Bing 搜索 workeronmessage = function(event){
var keyWord = event.data;
var results = searchBing(keyWord);
postMessage(results);
}
function searchBing(keyWord){
var bingAppId = "YOUR APP ID GOES HERE";
var url = "http://api.search.live.Net/JSon.ASPx?Appid="+
bingAppId+"&query="+keyWord+"&sources=web";
var xhr = new XMLHttpRequest();
xhr.open("GET", this.url, false);
xhr.send();
var response = JSON.parse(xhr.responseText);
var results = [];
var results = {};
var data = response.SearchResponse.Web.Results;
for (var i=0;i<data.length;i++){
result.url = data[i].Url;
result.title = data[i].Title;
result.description = data[i].Description;
results.push(result);
}
return results;
}
如您所見,這段代碼與 清單 3 中的代碼十分相似。它使用 onmessage/postMessage 范型來從它的母線程接收和發送數據。當然,這個 Web 服務的 URL 有點不同。返回的結構也有點差異,但您只需將它映射回清單 3 中的公共結構即可。可以想象得到,Yahoo Worker 的代碼與 Google 和 Bing 的 Worker 代碼將十分相似。只是 URL 有點不一樣,並且映射回常規化的數據結構的映射也有點不一樣。一旦數據進入了常規化的結構,就可以被傳遞給一個公共函數來更新用戶界面。
結束語
無論是現在還是將來,並行編程都將是開發一個成功應用程序的關鍵所在。能利用多線程來從其環境收獲最多的應用程序可以提供一種超級的用戶體驗。在本文中,您用一個非常簡單的示例,查詢了三個最受歡迎的搜索引擎。不過,可以想象得到同時從每個引擎檢索結果為用戶體驗所帶來的改進,而不是每次只從一個搜索引擎檢索結果。即使是在一個單 CPU 的計算機上,多線程也會帶來很大的不同,因為您不必花費大量時間等待 Web 服務的響應。加入到 JavaScript 1.9.1 中的 Worker API 使得為任何基於 XUL 應用程序進行這類編程變得十分容易。您可以將本文所展示的這種技術應用到 Firefox 擴展中或是使用 XULRunner 的桌面應用程序中。這些 API 很容易使用,並且您可以派生多個線程而又不必擔心系統會被鎖死。Workers API 為開發人員使用多線程來提高 XUL 應用程序的性能打消了所有顧慮。
本文示例源代碼或素材下載