JavaScript多線程編程簡介

作者: Daisuke Maki  來源: InfoQ  發布時間: 2008-08-13 17:52  閱讀: 3407 次  推薦: 7   [收藏]  

雖然有越來越多的網站在應用AJAX技術進行開發,但是構建一個復雜的AJAX應用仍然是一個難題。造成這些困難的主要原因是什么呢?是與服務器的 異步通信問題?還是GUI程序設計問題呢?通常這兩項工作都是由桌面程序來完成的,那究竟為何開發一個可以實現同樣功能的AJAX應用就這么困難呢?

AJAX 開發中的難題

讓我們通過一個簡單的例子來認識這個問題。假設你要建立一個樹形結構的公告欄系統(BBS),它可以根據用戶請求與服務器進行交互,動態加載每篇文 章的信息,而不是一次性從服務器載入所有文章信息。每篇文章有四個相關屬性:系統中可以作為唯一標識的ID、發貼人姓名、文章內容以及包含其所有子文章 ID的數組信息。首先假定有一個名為getArticle()的函數可以加載一篇文章信息。該函數接收的參數是要加載文章的ID,通過它可從服務器獲取文 章信息。它返回的對象包含與文章相關的四條屬性:id,name,content和children。例程如下:

function ( id ) {
     var a = getArticle(id);
     document.writeln(a.name + "
" + a.content);
 } 

然而你也許會注意到,重復用同一個文章ID調用此函數,需要與服務器之間進行反復且無益的通信。想要解決這個問題,可以考慮使用函數 getArticleWithCache(),它相當于一個帶有緩存能力的getArticle()。在這個例子中,getArticle()返回的數據 只是作為一個全局變量被保存下來:

var cache = {};
 function getArticleWithCache ( id ) {
     if ( !cache[id] ) {
         cache[id] = getArticle(id);
     }
     return cache[id];
 } 

現在已將讀入的文章緩存起來,讓我們再來考慮一下函數backgroundLoad(),它應用我們上面提到的緩存機制加載所有文章信息。其用途 是,當讀者在閱讀某篇文章時,從后臺預加載它所有子文章。因為文章數據是樹狀結構的,所以很容易寫一個遞歸的算法來遍歷樹并且加載所有的文章:

function backgroundLoad ( ids ) {
     for ( var i=0; i < ids.length; i++ ) {
         var a = getArticleWithCache(ids[i]);
         backgroundLoad(a.children);
     }
 } 

backgroundLoad ()函數接收一個ID數組作為參數,然后通過每個ID調用前面定義過的getArticldWithCache()方法,這樣就把每個ID對應的文章緩存 起來。之后再通過已加載文章的子文章ID數組遞歸調用backgroundLoad()方法,如此整個文章樹就被緩存起來。

到目前為止,一切似乎看起來都很完美。然而,只要你有過開發AJAX應用的經驗,你就應該知曉這種幼稚的實現方法根本不會成功,這個例子成立的基礎 是默認 getArticle()用的是同步通信。可是,作為一條基本原則,JavaScript要求在與服務器進行交互時要用異步通信,因為它是單線程的。就簡 單性而言,把每一件事情(包括GUI事件和渲染)都放在一個線程里來處理是一個很好的程序模型,因為這樣就無需再考慮線程同步這些復雜問題。另一方面,他 也暴露了應用開發中的一個嚴重問題,單線程環境看起來對用戶請求響應迅速,但是當線程忙于處理其它事情時(比如說調用getArticle()),就不能 對用戶的鼠標點擊和鍵盤操作做出響應。

如果在這個單線程環境里進行同步通信會發生什么事情呢?同步通信會中斷瀏覽器的執行直至獲得通信結果。在等待通信結果的過程中,由于服務器的調用還 沒有完成,線程會停止響應用戶并保持鎖定狀態直到調用返回。因為這個原因,當瀏覽器在等待服務器響應時它不能對用戶行為作出響應,所以看起來像是凍結了。 當執行 getArticleWithCache()和backgroundLoad()會有同樣的問題,因為它們都是基于getArticle()函數的。由于 下載所有的文章可能會耗費很可觀的一段時間,因此對于backgroundLoad()函數來說,瀏覽器在此段時間內的凍結就是一個很嚴重的問題——既然 瀏覽器都已經凍結,當用戶正在閱讀文章時就不可能首先去執行后臺預加載數據,如果這樣做連當前的文章都沒辦法讀。

如上所述,既然同步通信在使用中會造成如此嚴重的問題,JavaScript就把異步通信作為一條基本原則。因此,我們可以基于異步通信改寫上面的 程序。 JavaScript要求以一種事件驅動的程序設計方式來寫異步通信程序。在很多場合中,你都必須指定一個回調程序,一旦收到通信響應,這個函數就會被調 用。例如,上面定義的getArticleWithCache()可以寫成這樣:

var cache = {};
 function getArticleWithCache ( id, callback ) {
     if ( !cache[id] ) {
         callback(cache[id]);
     } else {
         getArticle(id, function( a ){
             cache[id] = a;
             callback(a);
         });
     }
 } 

這個程序也在內部調用了getArticle()函數。然而需要注意的是,為異步通信設計的這版getArticle()函數要接收一個函數作為第 二個參數。當調用這個getArticle()函數時,與從前一樣要給服務器發送一個請求,不同的是,現在函數會迅速返回而非等待服務器的響應。這意味 著,當執行權交回給調用程序時,還沒有得到服務器的響應。如此一來,線程就可以去執行其它任務直至獲得服務器響應,并在此時調用回調函數。一旦得到服務器 響應, getArticle()的第二個參數作為預先定義的回調函數就要被調用,服務器的返回值即為其參數。同樣的,getArticleWithCache ()也要做些改變,定義一個回調參數作為其第二個參數。這個回調函數將在被傳給getArticle()的回調函數中調用,因而它可以在服務器通信結束后 被執行。

單是上面這些改動你可能已經認為相當復雜了,但是對backgroundLoad()函數做得改動將會更復雜,它也要被改寫成可以處理回調函數的形式:

function backgroundLoad ( ids, callback ) {
     var i = 0;
     function l ( ) {
         if ( i < ids.length ) {
             getArticleWithCache(ids[i++], function( a ){
                 backgroundLoad(a.children, l);
             });
         } else {
             callback();
         }
     }
     l();
 } 

改動后的backgroundLoad()函數看上去和我們以前的那個函數已經相去甚遠,不過他們所實現的功能并無二致。這意味著這兩個函數都接受 ID數組作為參數,對于數組里的每個元素都要調用getArticleWithCache(),再應用已經獲得子文章ID遞歸調用 backgroundLoad ()。不過同樣是對數組的循環訪問,新函數中的就不太好辨認了,以前的程序中是用一個for循環語句完成的。為什么實現同樣功能的兩套函數是如此的大相徑 庭呢?

這個差異源于一個事實:任何函數在遇到有需要同服務器進行通信情況后,都必須立刻返回,例如getArticleWithCache()。除非原來 的函數不在執行當中,否則應當接受服務器響應的回調函數都不能被調用。對于JavaScript,在循環過程中中斷程序并在稍后從這個斷點繼續開始執行程 序是不可能的,例如一個for語句。因此,本例利用遞歸傳遞回調函數實現循環結構而非一個傳統循環語句。對那些熟悉連續傳送風格(CPS)的人來說,這就 是一個 CPS的手動實現,因為不能使用循環語法,所以即便如前面提到的遍歷樹那么簡單的程序也得寫得很復雜。與事件驅動程序設計相關的問題是控制流問題:循環和其它控制流表達式可能比較難理解。

這里還有另外一個問題:如果你把一個沒有應用異步通信的函數轉換為一個使用異步通信的函數,那么重寫的函數將需要一個回調函數作為新增參數,這為已 經存在的APIs造成了很大問題,因為內在的改變沒有把影響限于內部,而是導致整體混亂的APIs以及API的其它使用者的改變。

造成這些問題目的根本原因是什么呢?沒錯,正是JavaScript單線程機制導致了這些問題。在單線程里執行異步通信需要事件驅動程序設計和復雜的語句。如果當程序在等待服務器的響應時,有另外一個線程可以來處理用戶請求,那么上述復雜技術就不需要了。

試試多線程編程

讓我來介紹一下Concurrent.Thread,它是一個允許JavaScript進行多線程編程的庫,應用它可以大大緩解上文提及的在 AJAX開發中與異步通信相關的困難。這是一個用JavaScript寫成的免費的軟件庫,使用它的前提是遵守Mozilla Public License和GNU General Public License這兩個協議。你可以從他們的網站 下載源代碼。

馬上來下載和使用源碼吧!假定你已經將下載的源碼保存到一個名為Concurrent.Thread.js的文件夾里,在進行任何操作之前,先運行如下程序,這是一個很簡單的功能實現:

<script type="text/javascript" src="Concurrent.Thread.js"></script>
 <script type="text/javascript">
     Concurrent.Thread.create(function(){
         var i = 0;
         while ( 1 ) {
             document.body.innerHTML += i++ + "<br>";
         }
     });
 </script>

執行這個程序將會順序顯示從0開始的數字,它們一個接一個出現,你可以滾屏來看它。現在讓我們來仔細研究一下代碼,他應用while(1)條件制造 了一個不會中止的循環,通常情況下,象這樣不斷使用一個并且是唯一一個線程的JavaScript程序會導致瀏覽器看起來象凍結了一樣,自然也就不會允許 你滾屏。那么為什么上面的這段程序允許你這么做呢?關鍵之處在于while(1)上面的那條Concurrent.Thread.create()語句, 這是這個庫提供的一個方法,它可以創建一個新線程。被當做參數傳入的函數在這個新線程里執行,讓我們對程序做如下微調:

<script type="text/javascript" src="Concurrent.Thread.js"></script>
 <script type="text/javascript">
     function f ( i ){
         while ( 1 ) {
             document.body.innerHTML += i++ + "<br>";
         }
     }
     Concurrent.Thread.create(f, 0);
     Concurrent.Thread.create(f, 100000);
 </script> 

在這個程序里有個新函數f()可以重復顯示數字,它是在程序段起始 定義的,接著以f()為參數調用了兩次create()方法,傳給create()方法的第二個參數將會不加修改地傳給f()。執行這個程序,先會看到一 些從0開始的小數,接著是一些從100,000開始的大數,然后又是接著前面小數順序的數字。你可以觀察到程序在交替顯示小數和大數,這說明兩個線程在同 時運行。

讓我來展示Concurrent.Thread的另外一個用法。上面的例子調用create()方法來創建新線程。不調用庫里的任何APIs也有可能實現這個目的。例如,前面那個例子可以這樣寫:

<script type="text/javascript" src="Concurrent.Thread.js"></script>
 <script type="text/x-script.multithreaded-js">
     var i = 1;
     while ( 1 ) {
         document.body.innerHTML += i++ + "<br>";
     }
 </script> 

在script 標簽內,很簡單地用JavaScript寫了一個無窮循環。你應該注意到標簽內的type屬性,那里是一個很陌生的值(text/x- script.multithreaded-js),如果這個屬性被放在script標簽內,那么Concurrent.Thread就會在一個新的線程 內執行標簽之間的程序。你應當記住一點,在本例一樣,必須將Concurrent.Thread庫包含進來。

有了Concurrent.Thread,就有可能自如的將執行環境在線程之間進行切換,即使你的程序很長、連續性很強。我們可以簡要地討論下如何 執行這種操作。簡言之,需要進行代碼轉換。粗略地講,首先要把傳遞給create()的函數轉換成一個字符串,接著改寫直至它可以被分批分次執行。然后這 些程序可以依照調度程序逐步執行。調度程序負責協調多線程,換句話說,它可以在適當的時候做出調整以便每一個修改后的函數都會得到同等機會運行。 Concurrent.Thread實際上并沒有創建新的線程,僅僅是在原本單線程的基礎上模擬了一個多線程環境。

雖然轉換后的函數看起來是運行在不同的線程內,但是實際上只有一個線程在做這所有的事情。在轉換后的函數內執行同步通信仍然會造成瀏覽器凍結,你也 許會認為以前的那些問題根本就沒有解決。不過你不必耽心,Concurrent.Thread提供了一個應用JavaScript 的異步通信方式實現的定制通信庫,它被設計成當一個線程在等待服務器的響應時允許其它線程運行。這個通信庫存于 Concurrent.Thread.Http下。它的用法如下所示:

<script type="text/javascript" src="Concurrent.Thread.js"></script>
 <script type="text/x-script.multithreaded-js">
     var req = Concurrent.Thread.Http.get(url, ["Accept", "*"]);
     if (req.status == 200) {
         alert(req.responseText);
     } else {
         alert(req.statusText);
     }
 </script> 

get()方法,就像它的名字暗示的那樣,可以通過HTTP的GET方法獲得指定URL的內容,它將目標URL作為第一個參數,將一個代表HTTP 請求頭的數組作為可選的第二個參數。get()方法與服務器交互,當得到服務器的響應后就返回一個XMLHttpRequest對象作為返回值。當 get()方法返回時,已經收到了服務器響應,所以就沒必要再用回調函數接收結果。自然,也不必再耽心當程序等待服務器的響應時瀏覽器凍結的情況了。另 外,還有一個 post()方法可以用來發送數據到服務器:

<script type="text/javascript" src="Concurrent.Thread.js"></script>
 <script type="text/x-script.multithreaded-js">
     var req = Concurrent.Thread.Http.post(url, "key1=val1&key2=val2");
     alert(req.statusText);
 </script> 

post()方法將目的URL作為第一個參數,要發送的內容作為第二個參數。像get()方法那樣,你也可以將請求頭作為可選的第三個參數。

如果你用這個通信庫實現了第一個例子當中的getArticle()方法,那么你很快就能應用文章開頭示例的那種簡單的方法寫出 getArticleWithCache(),backgroundLoad ()以及其它調用了getArticle()方法的函數了。即使是那版backgroundLoad()正在讀文章數據,照例還有另外一個線程可以對用戶 請求做出響應,瀏覽器因此也不會凍結。現在,你能理解在JavaScript中應用多線程有多實用了?

7
0
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()