隨著AJAX范例得到越來越廣泛的應用,浏覽器頁面可以在向後台服務器請求數據的同時保持前端用戶界面的活躍性(因此在AJax中稱為異步)。然而,當這兩個活動同時訪問共用的JavaScript和DOM數據結構時就會引發問題。Javascript沒有提供針對該並發程序問題的經典解決方案。本文描述了作者在互斥機制方面的新見解,該經過驗證的互斥機制在JavaScript中能發揮良好的作用。
為什麼需要互斥?
當多個程序邏輯線程同時訪問相同數據的時候,問題便產生了。程序通常假定與其交互的數據在交互過程中不發生改變。訪問這些共享數據結構的代碼稱為臨界區,一次只允許一個程序訪問的機制被稱為互斥。在AJax應用程序中,當對來自XMLHttpRequest的應答進行異步處理的代碼同時操縱正在被用戶界面使用的數據時,便會發生這種情況。這個共用的數據可能是用於實現MVC數據模型的JavaScript和/或web頁面自身的DOM。如果二者中的任一個對共享數據做了不協調的更改,那麼二者的邏輯都將中斷。
也許您會說“等等,為什麼我沒有遇到過這種問題?”。遺憾的是,這種問題是同步依賴的(也叫做競態條件),因此它們並不總是發生,或者也許從不發生。它們的或然性基於許多因素。基於健壯性考慮,富internet應用程序應該通過確保這些問題不會發生來阻止出現這種情況。
因此,需要一種互斥機制來確保同時只能打開一個臨界區,並且在它結束之後才能打開另一個。在大多數主流計算機語言和執行框架中,都提供互斥機制(經常是幾種),但是應用於浏覽器端的Javascript卻沒有提供這種互斥機制。雖然存在一些無需專門的語言或環境支持的經典互斥實現算法,但是即使這樣還是需要一些JavaScript和浏覽器(如Internet Explorer)所缺少的要素。接下來介紹的經典算法在這些浏覽器和語言中能發揮良好的作用。
面包店算法
在計算機科學文獻中的幾種互斥算法中,所謂的Lamport面包店算法可以有效地用於多個相互競爭的控制線程,該算法中線程之間的通信只能在共享內存中進行(即,不需要諸如信號量、原子性的set-and-test之類的專門機制)。該算法的基本思想源於面包店,因為面包店需要先取號然後等候叫號。清單1給出了該算法的框架(引自Wikipedia),該算法可以使各線程進出臨界區而不產生沖突。
清單1. Lamport面包店算法偽代碼
// declaration & initial values of global variables
Enter, Number: array [1..N] of integer = {0};
// logic used by each thread...
// where "(a, b) < (c, d)"
// means "(a < c) or ((a == c) and (b < d))"
Thread(i) {
while (true) {
Enter [i] = 1;
Number[i] = 1 max(Number[1],...,Number[N]);
Enter [i] = 0;
for (j=1; j<=N; j) {
while (Enter[j] != 0) {
// wait until thread j receives its number
}
while ((Number[j]!=0) && ((Number[j],j) < (Number[i],i))) {
// wait until threads with smaller numbers
// or with the same number, but with higher
// priority, finish their work
}
}
// critical section...
Number[i] = 0;
// non-critical section...
}
}
// declaration & initial values of global variables
Enter, Number: array [1..N] of integer = {0};
// logic used by each thread...
// where "(a, b) < (c, d)"
// means "(a < c) or ((a == c) and (b < d))"
Thread(i) {
while (true) {
Enter [i] = 1;
Number[i] = 1 max(Number[1],...,Number[N]);
Enter [i] = 0;
for (j=1; j<=N; j) {
while (Enter[j] != 0) {
// wait until thread j receives its number
}
while ((Number[j]!=0) && ((Number[j],j) < (Number[i],i))) {
// wait until threads with smaller numbers
// or with the same number, but with higher
// priority, finish their work
}
}
// critical section...
Number[i] = 0;
// non-critical section...
}
}
如上所示,該算法假定各線程清楚自己的線程編號(常量i)和當前正在活動的線程總數(常量N)。此外,還假定存在一種等待或休眠方式,例如:暫時將CPU釋放給其他線程。遺憾的是,Internet Explorer中的Javascript沒有這種能力。雖然如此,如果實際運行在同一線程上的多個代碼部分表現為各自運行在獨立的虛擬線程上,那麼該面包店算法不會中斷。同樣,JavaScript具有一種在指定延遲後調度函數的機制,所以,可以使用下面的這些方法來優化面包店算法。
Wallace變體
在JavaScript中實現Lamport面包店算法的主要障礙在於缺少線程API。無法確定當前正在哪個線程上運行以及當前正在活動的線程數目,也無法將CPU釋放給其他的線程,無法創建新的線程來管理其他線程。因此,無法查證如何將特定的浏覽器事件(例如:單擊按紐、可用的XML應答等)分配到線程。
克服這些障礙的一種方法是使用Command設計模式。通過將所有應該進入臨界區的邏輯以及所有啟動該邏輯所需的數據一起放入到command 對象中,可以在負責管理command的類中重寫面包店算法。該互斥類僅在沒有其他臨界區(封裝為獨立的command對象方法)在執行時調用臨界區,就像它們各自運行在不同的虛擬線程中一樣。JavaScript的setTimeout()機制用於將CPU釋放給其他正在等待的command。
為command對象假定一個簡單的基類(見清單2中的Command),可以定義一個類(見清單3中的Mutex)來實現面包店算法的Wallace變體。注意,雖然可以通過很多方式在JavaScript中實現基類對象(為了簡潔起見,這裡使用一種簡單的方式),但是只要各個command對象擁有某個惟一的id,而且整個臨界區被封裝在單獨的方法中,那麼任何對象模式都可以使用這種方法。
清單2. 用於 Command 對象的簡單基類
1 function Command() {
2 if (!Command.NextID) Command.NextID = 0;
3 this.id = Command.NextID;
4 // unsynchronized API
5 this.doit = function(){ alert("DOIT called"); }
6 this.undo = function(){ alert("UNDO called"); }
7 this.redo = function(){ this.doit(); }
8 // synchronized API
9 this.sDoIt = function(){ new Mutex(this,"doit"); }
10 this.sUnDo = function(){ new Mutex(this,"undo"); }
11 this.sReDo = function(){ new Mutex(this,"redo"); }
12 }
1 function Command() {
2 if (!Command.NextID) Command.NextID = 0;
3 this.id = Command.NextID;
4 // unsynchronized API
5 this.doit = function(){ alert("DOIT called"); }
6 this.undo = function(){ alert("UNDO called"); }
7 this.redo = function(){ this.doit(); }
8 // synchronized API
9 this.sDoIt = function(){ new Mutex(this,"doit"); }
10 this.sUnDo = function(){ new Mutex(this,"undo"); }
11 this.sReDo = function(){ new Mutex(this,"redo"); }
12 }
Command類演示了三個臨界區方法(見5-7行),但是只要預先將對該方法的調用封裝在Mutex中(見9-11行),那麼就可以使用任何方法。有必要認識到,常規方法調用(例如非同步的方法調用)與同步方法調用之間存在著重要的區別:具有諷刺意味的是,必須保證同步方法不同步運行。換句話說,當調用sDoIt()方法時,必須確保方法doit()還未運行,即使方法sDoIt()已經返回。doit()方法可能已結束,或者直到將來的某一時間才開始執行。也就是說,將對Mutex的實例化視為啟動一個新的線程。
清單3.作為類 Mutex實現的 Wallace 變體
1 function Mutex( cmdObject, methodName ) {
2 // define static fIEld and method
3 if (!Mutex.Wait) Mutex.Wait = new Map();
4 Mutex.SLICE = function( cmdID, startID ) {
5 Mutex.Wait.get(cmdID).attempt( Mutex.Wait.get(startID) );
6 }
7 // define instance method
8 this.attempt = function( start ) {
9 for (var j=start; j; j=Mutex.Wait.next(j.c.id)) {
10 if (j.enter
11 || (j.number && (j.number < this.number ||
12 (j.number == this.number
13 && j.c.id < this.c.id))))
14 return setTimeout
15 ("Mutex.SLICE(" this.c.id "," j.c.id ")",10);
16 }
17 //run with exclusive Access
18 this.c[ this.methodID ]();
19 //release exclusive Access
20 this.number = 0;
21 Mutex.Wait.remove( this.c.id );
22 }
23 // constructor logic
24 this.c = cmdObject;
25 this.methodID = methodName;
26 //(enter and number are "false" here)
27 Mutex.Wait.add( this.c.id, this );
28 this.enter = true;
29 this.number = (new Date()).getTime();
30 this.enter = false;
31 this.attempt( Mutex.Wait.first() );
32 }
1 function Mutex( cmdObject, methodName ) {
2 // define static fIEld and method
3 if (!Mutex.Wait) Mutex.Wait = new Map();
4 Mutex.SLICE = function( cmdID, startID ) {
5 Mutex.Wait.get(cmdID).attempt( Mutex.Wait.get(startID) );
6 }
7 // define instance method
8 this.attempt = function( start ) {
9 for (var j=start; j; j=Mutex.Wait.next(j.c.id)) {
10 if (j.enter
11 || (j.number && (j.number < this.number ||
12 (j.number == this.number
13 && j.c.id < this.c.id))))
14 return setTimeout
15 ("Mutex.SLICE(" this.c.id "," j.c.id ")",10);
16 }
17 //run with exclusive Access
18 this.c[ this.methodID ]();
19 //release exclusive Access
20 this.number = 0;
21 Mutex.Wait.remove( this.c.id );
22 }
23 // constructor logic
24 this.c = cmdObject;
25 this.methodID = methodName;
26 //(enter and number are "false" here)
27 Mutex.Wait.add( this.c.id, this );
28 this.enter = true;
29 this.number = (new Date()).getTime();
30 this.enter = false;
31 this.attempt( Mutex.Wait.first() );
32 }
Mutex類的基本邏輯是將每個新的Mutex實例放入主等待清單,然後將其在等待隊列中啟動。因為每次到達“隊首”的嘗試都需要等待(除了最後一次),所以使用setTimeout來調度每次在當前嘗試停止的位置啟動的新嘗試。到達隊首時(見17行),便實現了互斥性訪問;因此,可以調用臨界區方法。執行完臨界區後,釋放互斥性訪問並從等待清單中移除Mutex實例(見20-21行)。
Mutex構造函數(見23-31行)記錄其Command對象和方法名參數,然後寄存在一個運行中臨界區的稀疏數組中(Mutex.Wait),這通過清單4中所示的Map類來實現。然後構造函數獲得下一個編號,並在隊尾開始排隊。由於等待編號中的間隔或副本不存在問題,所以實際上使用當前的時間戳作為下一個編號。
attempt()方法將初始偽代碼中的兩個wait循環組合成一個單獨的循環,該循環直到隊首時才對臨界區失效。該循環是一種忙碌-等待循環檢測方式,可以通過在setTimeout()調用中指定延遲量來終止該循環。由於setTimeout需要調用“無格式函數”,所以在第4-6行定義了靜態幫助器方法(Mutex.SLICE)。SLICE在主等待清單中查找指定的Mutex對象,然後調用其attempt()方法,用start參數指定到目前為止其所獲得的等待清單的長度。每次SLICE()調用都像獲得了“一塊CPU”。這種(通過setTimeout)適時釋放CPU的協作方式令人想到協同程序。
清單4. 作為 Map數據結構實現的稀疏數組
function Map() {
this.map = new Object();
// Map API
this.add = function( k,o ){
this.map[k] = o;
}
this.remove = function( k ){
delete this.map[k];
}
this.get = function( k ){
return k==null ? null : this.map[k];
}
this.first = function(){
return this.get( this.nextKey() );
}
this.next = function( k ){
return this.get( this.nextKey(k) );
}
this.nextKey = function( k ){
for (i in this.map) {
if ( !k ) return i;
if (k==i) k=null; /*tricky*/
}
return null;
}
}
function Map() {
this.map = new Object();
// Map API
this.add = function( k,o ){
this.map[k] = o;
}
this.remove = function( k ){
delete this.map[k];
}
this.get = function( k ){
return k==null ? null : this.map[k];
}
this.first = function(){
return this.get( this.nextKey() );
}
this.next = function( k ){
return this.get( this.nextKey(k) );
}
this.nextKey = function( k ){
for (i in this.map) {
if ( !k ) return i;
if (k==i) k=null; /*tricky*/
}
return null;
}
}
function Map() {
this.map = new Object();
// Map API
this.add = function( k,o ){
this.map[k] = o;
}
this.remove = function( k ){
delete this.map[k];
}
this.get = function( k ){
return k==null ? null : this.map[k];
}
this.first = function(){
return this.get( this.nextKey() );
}
this.next = function( k ){
return this.get( this.nextKey(k) );
}
this.nextKey = function( k ){
for (i in this.map) {
if ( !k ) return i;
if (k==i) k=null; /*tricky*/
}
return null;
}
}
function Map() {
this.map = new Object();
// Map API
this.add = function( k,o ){
this.map[k] = o;
}
this.remove = function( k ){
delete this.map[k];
}
this.get = function( k ){
return k==null ? null : this.map[k];
}
this.first = function(){
return this.get( this.nextKey() );
}
this.next = function( k ){
return this.get( this.nextKey(k) );
}
this.nextKey = function( k ){
for (i in this.map) {
if ( !k ) return i;
if (k==i) k=null; /*tricky*/
}
return null;
}
}
富Internet應用程序集成
由於Mutex所處理的線程(虛擬的或者非虛擬的)數量是動態變化的,所以可以確定一個基本事實:無法通過像浏覽器為各個浏覽器事件分配單獨的線程那樣的方式來獲得線程標識符。這裡做了一個類似的假定,那就是每個完整的事件處理程序組成一個完整的臨界區。基於這些假定,每個事件處理函數都可以轉變成一個command對象,並使用Mutex對其進行管理。當然,如果未將代碼明確組織成事件處理函數,那麼將需要重構。換句話說,不是直接在Html事件屬性中進行邏輯編碼(例如:onclick=' var'),而是調用事件處理函數(例如:onclick='FOO()'和function FOO(){ var;})。
清單5. 使用了非同步事件處理程序的示例web頁面
<Html>
<script language="JavaScript">
function newState(){
if (XMLreq.readyState==4) processReply();
}
function requestData(){
...set up asynchronous XML request...
XMLreq.onreadystatechange = newState;
...launch XML request...
}
function processReply(){
var transformedData = ...process data to Html...
OutputArea.innerHtml = transformedData "<br>";
}
function clearArea(){
OutputArea.innerHtml = "cleared<br>";
}
</script>
<body onload="requestData();">
<input type="button" value="clear" onclick="clearArea()">
<div id="OutputArea"/>
</body>
</Html>
<Html>
<script language="JavaScript">
function newState(){
if (XMLreq.readyState==4) processReply();
}
function requestData(){
...set up asynchronous XML request...
XMLreq.onreadystatechange = newState;
...launch XML request...
}
function processReply(){
var transformedData = ...process data to Html...
OutputArea.innerHtml = transformedData "<br>";
}
function clearArea(){
OutputArea.innerHtml = "cleared<br>";
}
</script>
<body onload="requestData();">
<input type="button" value="clear" onclick="clearArea()">
<div id="OutputArea"/>
</body>
</Html>
例如,假設有三個事件處理程序函數,它們操縱清單5所示的共用數據。它們處理頁面加載事件、單擊按鈕事件和來自XML請求的應答事件。頁面加載事件發出某個異步請求來要求獲取數據並指定請求-應答事件處理程序,該處理程序處理接收到的數據,並將其加載到共用數據結構。單擊按鈕事件處理程序也影響共用數據結構。為了避免這些事件處理程序發生沖突,可以通過清單6所示的Mutex將它們轉變成command並加以調用(假設JavaScript include文件mutex.JS中包含Map和Mutex)。注意,雖然可以使用優美的類繼承機制來實現Command子類,但是該代碼說明了最簡單的方法,該方法僅需要全局變量NEXT_CMD_ID。