隨著AJAX范例得到越來(lái)越廣泛的應(yīng)用,瀏覽器頁(yè)面可以在向后臺(tái)服務(wù)器請(qǐng)求數(shù)據(jù)的同時(shí)保持前端用戶界面的活躍性(因此在AJAX中稱為異步)。然而,當(dāng)這兩個(gè)活動(dòng)同時(shí)訪問(wèn)共用的JavaScript和DOM數(shù)據(jù)結(jié)構(gòu)時(shí)就會(huì)引發(fā)問(wèn)題。JavaScript沒(méi)有提供針對(duì)該并發(fā)程序問(wèn)題的經(jīng)典解決方案。本文描述了作者在互斥機(jī)制方面的新見(jiàn)解,該經(jīng)過(guò)驗(yàn)證的互斥機(jī)制在JavaScript中能發(fā)揮良好的作用。
為什么需要互斥?
當(dāng)多個(gè)程序邏輯線程同時(shí)訪問(wèn)相同數(shù)據(jù)的時(shí)候,問(wèn)題便產(chǎn)生了。程序通常假定與其交互的數(shù)據(jù)在交互過(guò)程中不發(fā)生改變。訪問(wèn)這些共享數(shù)據(jù)結(jié)構(gòu)的代碼稱為臨界區(qū),一次只允許一個(gè)程序訪問(wèn)的機(jī)制被稱為互斥。在AJAX應(yīng)用程序中,當(dāng)對(duì)來(lái)自XMLHttpRequest的應(yīng)答進(jìn)行異步處理的代碼同時(shí)操縱正在被用戶界面使用的數(shù)據(jù)時(shí),便會(huì)發(fā)生這種情況。這個(gè)共用的數(shù)據(jù)可能是用于實(shí)現(xiàn)MVC數(shù)據(jù)模型的JavaScript和/或web頁(yè)面自身的DOM。如果二者中的任一個(gè)對(duì)共享數(shù)據(jù)做了不協(xié)調(diào)的更改,那么二者的邏輯都將中斷。
也許您會(huì)說(shuō)“等等,為什么我沒(méi)有遇到過(guò)這種問(wèn)題?”。遺憾的是,這種問(wèn)題是同步依賴的(也叫做競(jìng)態(tài)條件),因此它們并不總是發(fā)生,或者也許從不發(fā)生。它們的或然性基于許多因素?;诮研钥紤],富internet應(yīng)用程序應(yīng)該通過(guò)確保這些問(wèn)題不會(huì)發(fā)生來(lái)阻止出現(xiàn)這種情況。
因此,需要一種互斥機(jī)制來(lái)確保同時(shí)只能打開(kāi)一個(gè)臨界區(qū),并且在它結(jié)束之后才能打開(kāi)另一個(gè)。在大多數(shù)主流計(jì)算機(jī)語(yǔ)言和執(zhí)行框架中,都提供互斥機(jī)制(經(jīng)常是幾種),但是應(yīng)用于瀏覽器端的JavaScript卻沒(méi)有提供這種互斥機(jī)制。雖然存在一些無(wú)需專門(mén)的語(yǔ)言或環(huán)境支持的經(jīng)典互斥實(shí)現(xiàn)算法,但是即使這樣還是需要一些JavaScript和瀏覽器(如Internet Explorer)所缺少的要素。接下來(lái)介紹的經(jīng)典算法在這些瀏覽器和語(yǔ)言中能發(fā)揮良好的作用。
面包店算法
在計(jì)算機(jī)科學(xué)文獻(xiàn)中的幾種互斥算法中,所謂的Lamport面包店算法可以有效地用于多個(gè)相互競(jìng)爭(zhēng)的控制線程,該算法中線程之間的通信只能在共享內(nèi)存中進(jìn)行(即,不需要諸如信號(hào)量、原子性的set-and-test之類的專門(mén)機(jī)制)。該算法的基本思想源于面包店,因?yàn)槊姘晷枰热√?hào)然后等候叫號(hào)。清單1給出了該算法的框架(引自Wikipedia),該算法可以使各線程進(jìn)出臨界區(qū)而不產(chǎn)生沖突。
清單1. Lamport面包店算法偽代碼
如上所示,該算法假定各線程清楚自己的線程編號(hào)(常量i)和當(dāng)前正在活動(dòng)的線程總數(shù)(常量N)。此外,還假定存在一種等待或休眠方式,例如:暫時(shí)將CPU釋放給其他線程。遺憾的是,Internet Explorer中的JavaScript沒(méi)有這種能力。雖然如此,如果實(shí)際運(yùn)行在同一線程上的多個(gè)代碼部分表現(xiàn)為各自運(yùn)行在獨(dú)立的虛擬線程上,那么該面包店算法不會(huì)中斷。同樣,JavaScript具有一種在指定延遲后調(diào)度函數(shù)的機(jī)制,所以,可以使用下面的這些方法來(lái)優(yōu)化面包店算法。
Wallace變體
在JavaScript中實(shí)現(xiàn)Lamport面包店算法的主要障礙在于缺少線程API。無(wú)法確定當(dāng)前正在哪個(gè)線程上運(yùn)行以及當(dāng)前正在活動(dòng)的線程數(shù)目,也無(wú)法將CPU釋放給其他的線程,無(wú)法創(chuàng)建新的線程來(lái)管理其他線程。因此,無(wú)法查證如何將特定的瀏覽器事件(例如:?jiǎn)螕舭醇~、可用的XML應(yīng)答等)分配到線程。
克服這些障礙的一種方法是使用Command設(shè)計(jì)模式。通過(guò)將所有應(yīng)該進(jìn)入臨界區(qū)的邏輯以及所有啟動(dòng)該邏輯所需的數(shù)據(jù)一起放入到command 對(duì)象中,可以在負(fù)責(zé)管理command的類中重寫(xiě)面包店算法。該互斥類僅在沒(méi)有其他臨界區(qū)(封裝為獨(dú)立的command對(duì)象方法)在執(zhí)行時(shí)調(diào)用臨界區(qū),就像它們各自運(yùn)行在不同的虛擬線程中一樣。JavaScript的setTimeout()機(jī)制用于將CPU釋放給其他正在等待的command。
為command對(duì)象假定一個(gè)簡(jiǎn)單的基類(見(jiàn)清單2中的Command),可以定義一個(gè)類(見(jiàn)清單3中的Mutex)來(lái)實(shí)現(xiàn)面包店算法的Wallace變體。注意,雖然可以通過(guò)很多方式在JavaScript中實(shí)現(xiàn)基類對(duì)象(為了簡(jiǎn)潔起見(jiàn),這里使用一種簡(jiǎn)單的方式),但是只要各個(gè)command對(duì)象擁有某個(gè)惟一的id,而且整個(gè)臨界區(qū)被封裝在單獨(dú)的方法中,那么任何對(duì)象模式都可以使用這種方法。
清單2. 用于 Command 對(duì)象的簡(jiǎn)單基類
Command類演示了三個(gè)臨界區(qū)方法(見(jiàn)5-7行),但是只要預(yù)先將對(duì)該方法的調(diào)用封裝在Mutex中(見(jiàn)9-11行),那么就可以使用任何方法。有必要認(rèn)識(shí)到,常規(guī)方法調(diào)用(例如非同步的方法調(diào)用)與同步方法調(diào)用之間存在著重要的區(qū)別:具有諷刺意味的是,必須保證同步方法不同步運(yùn)行。換句話說(shuō),當(dāng)調(diào)用sDoIt()方法時(shí),必須確保方法doit()還未運(yùn)行,即使方法sDoIt()已經(jīng)返回。doit()方法可能已結(jié)束,或者直到將來(lái)的某一時(shí)間才開(kāi)始執(zhí)行。也就是說(shuō),將對(duì)Mutex的實(shí)例化視為啟動(dòng)一個(gè)新的線程。
清單3.作為類 Mutex實(shí)現(xiàn)的 Wallace 變體
Mutex類的基本邏輯是將每個(gè)新的Mutex實(shí)例放入主等待清單,然后將其在等待隊(duì)列中啟動(dòng)。因?yàn)槊看蔚竭_(dá)“隊(duì)首”的嘗試都需要等待(除了最后一次),所以使用setTimeout來(lái)調(diào)度每次在當(dāng)前嘗試停止的位置啟動(dòng)的新嘗試。到達(dá)隊(duì)首時(shí)(見(jiàn)17行),便實(shí)現(xiàn)了互斥性訪問(wèn);因此,可以調(diào)用臨界區(qū)方法。執(zhí)行完臨界區(qū)后,釋放互斥性訪問(wèn)并從等待清單中移除Mutex實(shí)例(見(jiàn)20-21行)。
Mutex構(gòu)造函數(shù)(見(jiàn)23-31行)記錄其Command對(duì)象和方法名參數(shù),然后寄存在一個(gè)運(yùn)行中臨界區(qū)的稀疏數(shù)組中(Mutex.Wait),這通過(guò)清單4中所示的Map類來(lái)實(shí)現(xiàn)。然后構(gòu)造函數(shù)獲得下一個(gè)編號(hào),并在隊(duì)尾開(kāi)始排隊(duì)。由于等待編號(hào)中的間隔或副本不存在問(wèn)題,所以實(shí)際上使用當(dāng)前的時(shí)間戳作為下一個(gè)編號(hào)。
attempt()方法將初始偽代碼中的兩個(gè)wait循環(huán)組合成一個(gè)單獨(dú)的循環(huán),該循環(huán)直到隊(duì)首時(shí)才對(duì)臨界區(qū)失效。該循環(huán)是一種忙碌-等待循環(huán)檢測(cè)方式,可以通過(guò)在setTimeout()調(diào)用中指定延遲量來(lái)終止該循環(huán)。由于setTimeout需要調(diào)用“無(wú)格式函數(shù)”,所以在第4-6行定義了靜態(tài)幫助器方法(Mutex.SLICE)。SLICE在主等待清單中查找指定的Mutex對(duì)象,然后調(diào)用其attempt()方法,用start參數(shù)指定到目前為止其所獲得的等待清單的長(zhǎng)度。每次SLICE()調(diào)用都像獲得了“一塊CPU”。這種(通過(guò)setTimeout)適時(shí)釋放CPU的協(xié)作方式令人想到協(xié)同程序。
清單4. 作為 Map數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的稀疏數(shù)組
富Internet應(yīng)用程序集成
由于Mutex所處理的線程(虛擬的或者非虛擬的)數(shù)量是動(dòng)態(tài)變化的,所以可以確定一個(gè)基本事實(shí):無(wú)法通過(guò)像瀏覽器為各個(gè)瀏覽器事件分配單獨(dú)的線程那樣的方式來(lái)獲得線程標(biāo)識(shí)符。這里做了一個(gè)類似的假定,那就是每個(gè)完整的事件處理程序組成一個(gè)完整的臨界區(qū)?;谶@些假定,每個(gè)事件處理函數(shù)都可以轉(zhuǎn)變成一個(gè)command對(duì)象,并使用Mutex對(duì)其進(jìn)行管理。當(dāng)然,如果未將代碼明確組織成事件處理函數(shù),那么將需要重構(gòu)。換句話說(shuō),不是直接在HTML事件屬性中進(jìn)行邏輯編碼(例如:onclick='++var'),而是調(diào)用事件處理函數(shù)(例如:onclick='FOO()'和function FOO(){++var;})。
清單5. 使用了非同步事件處理程序的示例web頁(yè)面
例如,假設(shè)有三個(gè)事件處理程序函數(shù),它們操縱清單5所示的共用數(shù)據(jù)。它們處理頁(yè)面加載事件、單擊按鈕事件和來(lái)自XML請(qǐng)求的應(yīng)答事件。頁(yè)面加載事件發(fā)出某個(gè)異步請(qǐng)求來(lái)要求獲取數(shù)據(jù)并指定請(qǐng)求-應(yīng)答事件處理程序,該處理程序處理接收到的數(shù)據(jù),并將其加載到共用數(shù)據(jù)結(jié)構(gòu)。單擊按鈕事件處理程序也影響共用數(shù)據(jù)結(jié)構(gòu)。為了避免這些事件處理程序發(fā)生沖突,可以通過(guò)清單6所示的Mutex將它們轉(zhuǎn)變成command并加以調(diào)用(假設(shè)JavaScript include文件mutex.js中包含Map和Mutex)。注意,雖然可以使用優(yōu)美的類繼承機(jī)制來(lái)實(shí)現(xiàn)Command子類,但是該代碼說(shuō)明了最簡(jiǎn)單的方法,該方法僅需要全局變量NEXT_CMD_ID。
清單6. 轉(zhuǎn)化為同步事件處理程序的web頁(yè)面
已經(jīng)通過(guò)Mutex將這三個(gè)事件處理程序函數(shù)轉(zhuǎn)變?yōu)檎{(diào)用它們的初始邏輯(當(dāng)前都被預(yù)包裝于command類中)。各個(gè)command類定義一個(gè)獨(dú)特的標(biāo)識(shí)符和一個(gè)包含臨界區(qū)邏輯的方法,從而滿足了command接口的要求。
結(jié)束語(yǔ)
借助于AJAX和RIA,構(gòu)建復(fù)雜的動(dòng)態(tài)用戶界面的推動(dòng)力正在促使開(kāi)發(fā)人員使用先前與胖GUI客戶端緊密聯(lián)系的設(shè)計(jì)模式(例如:模型-視圖-控制器)。隨著視圖和控制器的定義模塊化,且每一個(gè)都帶有自己的事件和事件處理程序(除了共用數(shù)據(jù)模型),發(fā)生沖突的機(jī)率成倍提高。通過(guò)把事件處理邏輯封裝到Command類中,不僅可以使用Wallace變體,而且為提供豐富的撤消/重做功能、腳本編寫(xiě)界面和單元測(cè)試工具創(chuàng)造了條件。
新聞熱點(diǎn)
疑難解答
圖片精選