所謂的「同步(Synchronous)」,和「非同步(Asynchronous)」,是我們在撰寫程式或是開發系統時,常會聽到的形容詞。這兩種概念可以用在許多地方,包括描述程式設計的模式,所以,我們會看到所謂「同步程式設計(Synchronous Programming)」,以及「非同步程式設計(Asynchronous Programming)」。這兩個名詞分別描述了兩種不同的程式設計模式,但它們之間有什麼差別呢?

目前的IT系統先天上都是非同步
我們在做程式設計的時候,通常會涉及要處理多個工作,而所謂的「同步程式設計」,就是要執行的這些工作彼此之間是有「同步」的關係的。「同步」,顧名思義,就是不同工作間前進的步伐協同一致。因為不同的工作執行的步調不同,為了要協調彼此間的步伐,所以工作間會有必須等候其他工作完成的情況,而這「等待」就成了同步程式設計的一個重要特質。

對單一處理器的系統而言,就處理器本身實際上同時只能處理一項動作,所以,處理器循序的執行指令先天就是同步的(事實上也沒有什麼其他的工作可和其發生同步的關係)。但是,即便如此,這樣的系統在運行時,其本質還是非同步的。

怎麼說呢?因為一個實際的系統,不單只有涉及處理器,還有眾多的輸入、輸出設備,像是鍵盤、硬碟、網路卡、……等等。這些輸入輸出的設備,其工作的運行都是獨立於處理器之外,它們並不倚賴處理器執行指令才能運作。但是,最終它們還是必須和處理器所執行的指令發生關係。就像我們透過處理器所執行的程式,想要讀取來自於硬碟中的資料,這些資料終究還是必須進到系統的主記憶體中,以便程式碼加以存取,如此一來,硬碟設備處理資料的工作,就和處理器所執行的工作發生了關係。你可以想像,硬碟設備自行處理資料的工作,和處理器工作是平行運作的,處理器可以一邊執行指令,而硬碟設備也可以同時將資料讀取至主記憶體中。這正是其先天非同步的本質。

但是問題來了,即使這二者間的執行是相互獨立、平行,但處理器所執行的程式終究還是得讀取到資料,而這讀取到資料的模式就可能會有同步,還有非同步等兩種。

如果是非同步的模式,那就是和先天的特質一致。在非同步的模式底下,程式讀取資料的方式像是「我想要讀得資料,但是我不想等待,當有資料被讀取進來時,請告訴我,讓我可以做一些事」。所以,我們所設計的程式有一個自己的主要執行流程,但是,它並不會在任何時刻「等待」資料的到來。相反的,資料到來時,程式會被以某種方式「通知」,以利它取得資料並且加以處理。

而在同步的模式底下,程式讀取資料的方式就變成了「請讓我讀得到資料」。所以當程式的主要流程要求讀取資料時,就會持續「等待」,直到的確存在資料可供讀取為止。

同步模式的優缺點

你或許在上述的例子中會發現,同步的程式設計模式看起來比較單純,而且比較直覺(當然,在某些情況下,可能是非同步的模式較為直覺)。當程式想讀取資料時,就表示它想要讀得資料的意圖,等到該請求被滿足時,便已取得資料(或發生錯誤)。非同步的程式設計模式就複雜許多了,因為可能負責使用資料的主流程,和可讀得資料的流程是獨立運行,二者之間必須透過其他的機制來「同步」,才能使得主流程得以操作資料。

從C、C++、Java等常見語言的程式庫來看,在讀寫、輸入輸出設備資料的部份,都是採取同步模式,便可以明白,這種方式的確對設計程式比較便利,而且直覺。

不過,同步模式並非沒有缺點。雖然因為此種模式先天就是同步各工作、使流程循序執行,但是,這也使得主流程必須不斷「等待」各工作的完成。有些應用情境下,這並不打緊,因為就算採非同步模式,還是得處理工作間「同步」的動作。但是有些應用情境下,這「等待」會衍生出問題。

例如,對於一個必須即時反應來自使用者的輸入,或是對使用者輸出的程式,若是在主流程中發生了等待的停滯情況,那麼無論是讀取來自使用者的輸入,或是更新對使用者的輸出,都會因為這停滯而無法處理。尤其對於圖形化操作介面的程式而言,這種情況就會像是使用者反覆不斷嘗試移動滑鼠,卻看不到滑鼠的游標移動,甚至畫面還因為停止更新,而變成了蒼白的一片。為止,採用非同步的程式設計模式,多半是比較好的選擇。

從這邊你也可以觀察到一個有趣的現象,像Windows這種以GUI為主的作業系統家族(尤其早期Windows 3.1之前,更是非強制性多工的作業系統),其系統的API反而就考量到非同步的需求,使同步及非同步的模式並行及兼容。這當然是為了讓面對GUI程式的設計者,能夠設計出能即時反應使用者輸入及輸出的應用程式而做的決定。

非同步讀取的三種方式

正如前段文中所提到的,非同步的程式設計模式通常是比較複雜的。舉例來說,同步的讀取,只需要呼叫讀取的函式,待函式成功返回之後,就能取得資料並以加以操作。而非同步的讀取,在呼叫讀取的函式後立即返回,資料會在之後透過某種途徑讓程式得以操作。

那麼可以透過那些途徑呢?一般常見的方式,有下列三種。

第一種就是「等待直到完成」。你或許會覺得很奇怪,既然都要等待了,為什麼還要採用非同步的模式呢?不過,這二者間其實還是有所差異。因為當你採用非同步的模式時,當你在主流程中啟動讀取動作之後,你還是有機會做一些其他的事情,直到你確定主流程沒有其他的動作可做,必須等待資料到來,才能繼續下一步的動作時,你就可以使用這種方式持續等到資料的到來。而這毫無疑問的當然是一個達成同步的操作。

而第二種就是所謂的「回呼(callback)」,在這種模式下,主流程在啟動非同步讀取動作的同時,也一併指定了一個回呼的處理常式(callback handler),例如在C語言中,它可能就是個函式指標。啟動之後,主流程便可以繼續執行其他的動作,當資料讀得之後,所指定的回呼處理常式便會被呼叫,並且被傳入和資料有關的各項資訊,例如所讀得資料的長度以及內容。由於這個回呼的處理常式並不在主流程中(若從執行緒的角度來看,它就是在主執行緒之外的另一個獨立執行緒中),所以,它所取得的資料和主流程之間,還是必須透過某種同步的機制來溝通,才能使得主流程得以妥善的運用所讀的資料。

第三種方式則被稱為「輪詢(polling)」。顧名思義,其動作就是反覆不斷的詢問,詢問是否有可讀的資料。當啟動非同動的讀取動作之後,主流程仍然可以執行其他的動作,但是在他需要資料的時候,它可以不斷的詢問是否已有可供讀取的資料,一直問到資料讀得為止。

從上述的三種方式你不難發現,即使資料的讀取本質上本來就是非同步的,但是在程式中操作資料時,最終還是需要一個同步的動作,才能使主流程和資料取得兩件事「協同步伐」。

不論是同步或是非同步的程式設計模型,都各自有適合的應用情境。了解其本質及運作的特性,才能夠更輕易的駕馭它們。

 

專欄作者

熱門新聞

Advertisement