當有人提到Parallel(平行運算)或Concurrency(共時運算)時,在加入他們的話題前,你最好先搞清楚他說的是哪一種層次。共時分成四種層次:指令層次、敘述層次、單元層次、程式層次。指令的共時,是指同時執行兩道以上的處理器指令;敘述的共時,是指同時執行兩道以上的敘述;單元的共時,是指同時執行兩個以上的次程式(Sub-program);而程式的共時,是指同時執行兩個以上的程式。

處理器與編譯器的設計者,會比較關心「指令」的共時。作業系統的設計者,會比較關心「程式」的共時。而一般編程語言的使用者,則比較關心「敘述」與「單元」的共時。其中,因為「敘述」共時語言(例如rhope)並不多見(因為設計這種語言的難度比較高),所以焦點更是放「單元」共時,「單元」共時語言才是目前的主流。

關於單元共時,有兩種不同的作法,分別是「共享狀態共時」與「訊息傳遞共時」。大多數的主流語言(Java、C#、C++)採用「共享狀態共時」的設計;但Erlang、Oz、Occam則走向另一條人煙罕至的路,採用「訊息傳遞共時」。

「共享狀態共時」採用執行緒(線程),牽涉到「可變狀態」(記憶體可以被改變)。如果只有一個線程會去改變狀態,這不會有問題;但如果有多個線程共享記憶體,而且會去修改相同的記憶體,那就糟了!資料可能會被其他線程所污損、破壞。

為了要保護共享記憶體,避免同時修改,「共享狀態共時」會提供上鎖的機制,可能是Mutex、同步化方法(Synchronized Method)或其他。但不管怎樣,其實內在就是一把鎖。例如Java語言雖然採用同步化方法,但其實Java虛擬機器內部是使用Mutex。

有鎖,就會有人遺失鑰匙。當你掉了鑰匙時,你會感到驚慌、不知所措。軟體系統的鎖出問題時,也是如此。分散式軟體系統只要有鎖和鑰匙,不出錯的機會很低。

如果在Critical Region/Section內當機,事情就嚴重了,許多程式會永遠動彈不得。如果忘了上鎖,導致程式污損了共享狀態,會造成程式的行為舉止失控、變得瘋狂。如果上鎖的次序沒有處理好,那麼OS課本裡面的經典範例「哲學家用餐」進入deadlock(僵局)而餓死的慘劇,也會在你的程式中出現。

共享狀態的方式會引發許多潛在問題,寫程式時必須步步為營,否則一旦有bug,會相當難以除錯。有人用「千頭萬緒」描述「共享記憶體多執行緒」的狀況,可謂相當貼切巧妙。

編程員可以修正這些問題,只是難度很高。在單核心的處理器上,寫段程式或許是可行的解決之道;但是到了多核心的環境,問題往往會再度浮現。解決方案相當多(交易型記憶體可能是最好的方案),但是這需要進行相當的拼湊,一不小心,又會是另一個惡夢的開端。

在訊息傳遞的共時中,沒有共享狀態,唯一交換資料的方式,是透過「非同步」訊息傳遞。以Erlang來說,每個共時的單位稱為行程(Process),但所謂的「行程」,其實是類似執行緒的地位,而不是真正的OS行程。Erlang的行程之間彼此透過訊息傳遞方式進行溝通。每個行程都可以在不同的核心、不同處理器,甚至不同的電腦上執行。

Erlang不具有可變資料結構,不需要上鎖,平行化很容易。Erlang要如何平行化?很簡單,編程員將問題的解決方案拆解成許多平行的行程。這種編程風格稱為「共時導向編程」(Concurrency-Oriented Programming,COP)。

別被「共時導向編程」這名稱嚇著了,畢竟我們大腦天生就是共時專家。藉由腦部稱為杏仁核的區塊,我們對於外界的刺激能極快地反應,沒有快速的反應,我們可能會死亡,畢竟意識的反應相當慢。

如果我們想要寫出來的程式,作用像是真實世界的另一個物件的行為,那麼這些程式就必須有共時的結構。這就是為何我們必須用共時編程語言來寫程式的原因。但是在真實的世界,我們卻最常使用序列式(Sequential)編程語言,這導致共時編程的難度沒有必要地提高了。

使用「專為開發共時應用」而設計的語言,共時的開發就會容易許多。尤其是最近開始流行的Erlang,讓我們能將思考與互動的方式塑造成模型。

現在我們需要開發的系統,往往得橫跨網路上多部機器,而一個機器上有多個處理器、一個處理器有多個核心、一個核心有多個超執行緒。越來越多人想要充分運用這樣的運算環境,這也就是共時導向編程會逐漸受到重視的原因。

作者簡介:
蔡學鏞-技術顧問
清華大學資訊工程碩士,曾任華碩集團軟體工程師、元智大學資訊系講師、美商歐萊禮出版社技術編輯、臺灣微軟特約專欄作家。

熱門新聞

Advertisement