顧名思義,「同步」就是協同步伐,但是,不同的工作因為性質不同,需要的時間也不同,即使可以並行執行,所需的執行時間也不盡相同。協同步伐指的就像是A工作和B工作同時進行,但A工作需要B工作完成後的結果,因此,A工作和B工作之間必須協同步伐,也就是同步。說的更具體一點,A工作必須等待B工作完成後,依據其結果來繼續執行,這就是同步的本質和特性。

相反的,在非同步的程式模型底下,則不會採用這種持續等待的方式。雖說A工作終究是需要得到B工作的執行結果,以便進行其他的活動,但是A工作並不持續等待,它可以繼續執行,但透過其他的途徑察知B工作是否完成,以及完成後的結果。在前文中提到,在非同步的程式模型底下,可以透過「等待直到完成」、「回呼(callback)」以及「輪詢(polling)」,來得到非同步執行之工作的完成與否及結果。

一般來說,同步程式設計模式在概念上簡單,因為工作間循序執行,得到一個工作的結果後,才繼續執行下一個。但是,因為需要等待,一來可能缺乏效率(等待使得程式失去了執行其他工作的機會),二來,等待的時間難以估量,有些應用情境不允許無限制或長期的等待。

舉例來說,對基於HTTP協定的Web應用程式而言,收到來自於客戶端的HTTP請求之後,Web應用程式執行對應的動作,再回傳HTTP回應內容。由於客戶端等待連線的時間通常會有逾時值,超過這個逾時值之後,即使Web應用程式尚在執行,客戶端(例如瀏覽器)就會放棄連線。因此,對於採用同步程式設計模型的Web應用程式,若是處理動作過久,就會衍生出無法及時回應客戶端的問題。

為此,就必須採用非同步的程式設計模型,在收到來自於客戶端的請求時,便以非同步的方式啟動工作的進行,但在該工作未完成前,先回覆客戶端。此時的意義像是「工作請求已收到,敬請等待執行結果(通常還會回傳一個Task ID以資識別)」。那麼,客戶端要怎麼得知工作完成與否,以及結果呢?雖然這是跨行程、甚至是跨機器的主從架構,但是一樣可以使用「等待直到完成」、「回呼」、以及「輪詢」這幾種方式。

以瀏覽器對Web應用伺服器上的應用程式來說,最常見的大概就是輪詢。使用輪詢的方式,便是客戶端定期向伺服器端的應用程式,查詢所指定Task ID對應的工作是否完成,以及其完成結果。

對於一些B2B的應用來說,由於輪詢比較耗用資源,同時,受限在輪詢的週期,若週期太長,則反應時間就拉長,若週期太短,耗用資源就更多,所以,這些應用並不採用輪詢的方式,而是採用回呼的方式。在回呼的方式下,當客戶端請求執行工作的時候,會提供伺服器一個回呼的位置(對Web應用程式來?,多半就是另一個HTTP的URL),而伺服器端會暫時回應,並回傳允許客戶端日後對應至該工作的相關資訊,例如Task ID。另一方面,伺服器端會執行工作,待工作完作後,便會依據客戶端當初提供的回呼位置,連線回去,並且提供工作執行的結果。這時候,當初的客戶端程式,便可因此而收到通知。

使用回呼的方式,不僅節省資源(不需要反覆地不斷請求)而且即時,不會受限在輪詢的週期長短。但是,對於客戶端來說,就需要提供可供連線的伺服器條件。所以,像以瀏覽器為基礎的客戶端就不適合,而像B2B這種,其實是伺服器對伺服器的應用中,卻相當適合。

非同步應用的範圍很廣

在上述的例子中,非同步的模型應用在跨越行程、跨越機器的情境下。事實上,即使是同一行程中的不同執行緒間,一樣可以套用相同的概念。

在同一個行程中,要進行非同步的作業,多執行緒是一個常見的實作方式。當主執行緒A打算執行某個工作B時,它可以產生另一個獨立的執行緒以執行工作B,而自己仍可在不停滯等待的情況下,繼續執行其他的工作。當然,主執行緒A很有可能必須知道工作B是否完成,以便處理不同的工作或做不同的決策。那麼它要怎麼知道呢?它可以運用像是「等待直到完成」,以及「輪詢(polling)」這幾種方式。

當執行緒A已經完成所有必要的工作,只剩等待B的完成後,它可以利用多執行緒程式庫或API提供的函式,來等待執行B工作的執行緒的結束。在該執行緒結束之後,便可以結束等待,取得執行結果並且繼續執行。這就是「等待直到完成」。

執行緒A也可以和B約定特定的變數來做為輪詢的對象。在啟動B的執行之後,A可以在需要時,查詢所約定變數之值。若該變數之值已指出B的執行完成或到達特定階段,A便可以處理不同的工作或做不同的決策。

不論是單一行程的多執行緒下,或跨越行程、跨越機器的網路應用程式間的通訊,非同步的設計觀念都是相同的,只是採取的實作手法有所變化。

當非同步工作太多時,怎麼辦?
此外,有些時候我們可能會同時要求多個非同步工作的執行,但是,受限於資源(例如計算力不足),所以,同時間只能執行一定數目的非同步工作。這時候,時常會運用所謂的工作佇列(Task Queue或Job Queue)。當要求非同步工作的執行時,便將相關的工作資訊置入工作佇列中,而每當有閒置可用的計算資源,例如執行緒時,便從工作佇列中取出工作資訊,並且加以執行。而實作工作佇列的方式也有很多種,可以使用資料庫來自行實作,也有很多人使用專門的訊息佇列伺服器(Message Queue)。

此外,為了控制同時間用於非同步工作的計算資源,通常也會利用像Process Pool或Thread Pool的機制,透過Pool的容量,便可以控制用於非同步工作的計算資源。當我們將工作佇列和Thread Pool搭配使用時,便會有一個角色,負責檢視Thread Pool中是否有可用的Thread,倘若有,便自工作佇列中取出工作並執行。

採用多執行緒的方式來做非同步的設計,基本上對主執行緒而言是非同步,但在處理各工作的執行緒中,仍是同步的觀念。正如前段文字中所提到的,同步的設計模型優點是簡單較容易處理。所以使用同執行緒時,便可以藉此得到這個優點,只不過仍有可能得處理執行緒間的同步問題。

像在網路伺服器的設計中,同一個伺服器必須同時間服務多個客戶端,所以其本質必須是非同步的(若是同步,則一次只能服務一個)。若採多執行緒的方式來設計,在主執行緒接收到來自客戶端的連線請求時,便可以產生一個新的執行緒(或從Thread Pool中取得)來專門服務該連線請求。在服務該連線的過程中,概念上還是同步的,所以這種模型在概念上很簡單也容易設計。

還有另一種設計方式,便是只利用一個執行緒來達成伺服器的實作。但因為要同時服務多個連線請求,所以主執行緒不能夠受任何同步的操作影響,而停滯執行。為此,所有操作必須是非同步的。所以像Socket的各項操作,包括接受連線、讀取、寫作等等,都不能夠造成等待的情況。如果你熟悉Socket的程式設計,你就會知道這通常是利用select()函式來做到。這樣的控制,程式模型就會變複雜許多,但是優點則是節省資源,只需要利用一個執行緒,就可以服務多個客戶端的連線請求。

由上例可見,同一種應用,還是可以利用不同的設計模型來達成,只是需要視其實際的需求,還有應用的限制,來決定究竟採用那一種較為適合。

 

專欄作者

熱門新聞

Advertisement