免費的午餐已經結束,你準備好了嗎?

免費的午餐已經結束,你準備好了嗎?
作者:楊小華
引子
2005年3月,C++大師Herb Sutter在Dr.Dobb’s Journal上發表了一篇名爲《免費的午餐已經結束》的文章,一石激起千層浪,該文引起了社區廣大程序員的熱烈討論。文章指出:現在的程序員對效率、伸縮性、吞吐量等一系列性能指標相當忽視,很多性能問題都依仗越來越快的CPU來解決。但CPU的速度很快將偏離摩爾定律的軌跡,並達到一定的極限。所以,越來越多的應用程序將不得不直面性能問題。而解決這些問題的辦法就是採用併發編程技術。當你讀到這裏的時候,第一感覺可能就是“不敢苟同”,覺得作者在危言聳聽,妖言惑衆過分渲染併發編程的重要性。
其實不然,正如Herb Sutter所說,由於串行處理速度的限制已經把“併發編程”推到了聚光燈下,串行化技術在程序設計中的砥柱地位在未來必將被取代,一個多核與併發編程的時代必將到來。由於當今大多數程序員對併發編程還是一片空白,因此深入瞭解和學習併發編程已經刻不容緩。甚至,還有人提出了“不懂併發編程的程序員,不是一個合格的程序員”的觀點。不管願意接受與否,“免費的午餐”的確已經結束。
何爲併發
大家也許還記得那個懵懂的中學時代吧,手捧課本,端坐教室,聽華羅庚大師講“燒水沏茶”的故事。他將統籌學原理運用到日常生活中,竟然產生事半功倍的效果,給了我們很大的啓示。手敲鍵盤之際,調試程序之餘,與同事神侃之時,我們是否應該坐下來靜靜地思考一下,能否將“燒水沏茶”的道理運用到程序設計與開發的過程中呢?能否在編寫程序的時候把類似於劈材、打水、燒水、拿茶葉、泡茶等一系列的程序行爲併發執行?答案是:完全可以。這正是“併發編程”的絕妙之處,也是本文將要給大家介紹的內容。
首先,我們來看一看“何爲併發”。如果兩個事件在同一時間間隔內發生就稱之爲併發(concurrency)。兩個或多個任務在同一時間間隔內執行叫做併發執行。
我們再通過一個現實生活中的例子來闡述併發的機理。現在滿大街都是減肥廣告,減肥成了衆多人樂此不疲的話題。但是正確的減肥方法並不能單純地依靠節食或藥物,一個好的減肥方案往往需要同時考慮“適當的節食”和“一定強度的鍛鍊”。我們可以把“節食”和“鍛鍊”看作是併發執行的任務,也就是說改進飲食結構和正規的身體訓練必須要在同一時間間隔內發生,這樣纔可以達到減肥的目的。通過這個例子,我們可以得出這樣一個結論:任何一個邏輯控制流和另外的邏輯控制流在某一時間段內相互重疊,這就是併發。併發技術使得應用程序在同一時間段內能夠做更多的工作,極大地提高了效率,同時也增強了程序的可伸縮性。
軟件併發的基本層次
併發總體上可分爲三個層次:軟件級的併發、操作系統級的併發和硬件級的併發。一般來講,硬件級的併發和操作系統級的併發都會支持軟件級的併發,由於操作系統級併發和硬件級的併發不是我們普通程序員所能支配的,所以我們着重關注軟件級的併發。
並行和分佈式編程是達到軟件級併發的兩種基本途徑。它們是兩種不同的,但有時又相互交叉的編程方法。並行編程技術是將程序分配給單個或多個處理器運行,這些處理器通常在某一個物理或虛擬的計算機內;而分佈式編程技術是將程序分配給兩個或多個處理器運行,這些處理器可能在也可能不在同一個計算機中。在純粹的並行程序中,併發執行的部分往往都是同一個程序中的某些部分,而在分佈式程序中,這些併發執行的部分通常往往被實現成分離的程序。這兩者之間的區別及程序的典型結構如圖1和圖2所示:

圖1 並行程序的典型系統結構
圖2分佈式程序的典型系統結構
一般而言,軟件級的併發可分爲如下幾個層次:
1.         指令級的併發
當一條單指令中的多個部分被同時執行時,便產生了指令級的併發。這種併發執行對象的劃分粒度是最小的,以指令或指令中的某一部分爲單位。下面我們來看一個簡單的指令級併發實例,如圖3所示。
圖3 指令級併發實例
例如代碼中的(a+b)和(c-d)部分,就能夠同時執行。這種併發通常由編譯器指令所支持,而不受程序員的直接控制。關於指令級的併發,可以參看《Intel C++9.0 邁向多核CPU時代的終極優化利器》(2005年第7期《程序員》)一文中的“編譯器自動並行化”部分。
2.         例程(函數/程序)級的併發
如果應用程序中的某一邏輯可以分解成若干互不相干的函數,那麼就可以將這些函數分配給不同的進程或線程,讓這些函數併發執行來提高工作效率。這種併發執行對象的劃分粒度較小,以函數爲單位,次之於指令級併發。在實際的程序開發過程中,這種級別的併發是最常見的一種。筆者曾經參與過一個項目,所負責的部分裏有一個模塊要求將各種圖像格式轉換成JPG格式,同時獲取相關圖像數據(長、寬、高、角版/切版、DPI值等),並寫入數據庫中。這個模塊最終就是用函數級的併發技術完成的。設置一個函數完成圖像轉換,另一個函數完成圖像數據的獲取,同時寫入數據庫。將這兩個函數分別分配給不同的線程來執行,並在一個合適的點進行同步,如果任何一個函數失敗,那麼都將刪除另外一個已經生成的記錄。
3.         對象級的併發
這種併發執行對象的劃分粒度較大,通常以對象爲單位。當然,這些對象要滿足一定的條件,不違背流程執行的先後順序。如果滿足以上條件,我們就可以把每個對象分配給不同的進程或線程,根據一些中間件標準,比如CORBA、ICE等,每個對象甚至可以被分配給同一網絡上的不同計算機或不同網絡上的不同的計算機上執行,這樣就實現了對象級的併發執行。
4.         應用級的併發
這一級別的併發,相信大家並不陌生。現代操作系統都能同時並行運行數個應用程序,比如,筆者在鍵盤上敲下上面這些文字的同時,耳朵上還帶着耳機,欣賞着美妙的音樂,這不就是典型的應用級的併發嗎?這種級別的併發性可以看作是一種將單個內核用來運行多個應用程序的策略。
構建併發程序的幾種機制
從上文可以看出,併發性不僅僅侷限於內核,它也可以在應用程序中扮演重要角色。如例程級的併發,基於例程級併發的應用程序稱爲併發程序(concurrent program)。例程級的併發在很多方面都有其優勢,如在多處理器上並行計算、訪問慢速設備、人機交互和爲多個網絡客戶端提供服務等等。
現代操作系統提供了三種基本的構造併發程序的機制,以下所講述的原理不僅僅侷限於windows操作系統。
1.         進程
這種併發性是指通過將程序分解成多個進程來完成,即每個邏輯控制流都是一個進程,由內核負責調度和維護。因爲進程有獨立的虛擬地址空間,想要和其他進程進行通信,則需要使用某種顯式的進程間通訊(IPC)機制,這是基於進程設計方法的一個缺點。基於進程設計的另外一個缺點就是往往速度比較慢,因爲進程控制和IPC的開銷都很高。
2.         I/O多路複用
在這種形式的併發編程中,應用程序在一個進程的上下文中顯式地調度它們自己的邏輯流。邏輯流被模型化爲狀態機,作爲數據到達文件描述符的結果,主程序顯式地從一個狀態轉換到另一個狀態。因爲程序是一個單獨的進程,所以所有的流都共享同一地址空間。
對Unix /Linux熟悉的讀者對這一機制並不陌生。在Linux下提供了select/poll/epoll等各種方法,在BSD一類系統中,還提供了Kqueue方法。
I/O多路複用技術可以用作併發事件驅動程序的基礎。在事件驅動中,流是作爲某種事件的結果前進的。服務器使用I/O多路複用,藉助select之類的函數,檢測事件的發生。很明顯,採用這類方法設計程序,將使程序變得很複雜。
3.         線程
除了將程序分解成多個進程來執行外,還可以分解成多個線程來執行。線程是運行在一個單一進程上下文中的邏輯流,由內核負責調度。一個線程就是運行在一個進程上下文中的一個邏輯流。如典型的windows完成端口IOCP,就是利用線程池來提供服務。
併發程序設計的難點
在軟件開發和設計過程中,串行化編程的思想已經根深蒂固,以至於很多程序員都發覺難以適應,仍然固守着串行編程的習慣。然而在並行編程世界中所有的一切都已經發生了變化。在並行編程世界中,程序可以被分解成多個任務,並且每個任務都可以在相同的時間點執行,每個任務又可以被分配給多個線程來執行。程序執行的順序和位置通常是不可預知的,多個任務能夠在任意處理器上同時開始執行,但是不確定首先完成哪個任務,按照什麼次序來完成這些任務,以及由什麼處理器來執行這些任務。除了多個任務能夠並行執行外,單個任務也可能具有能同時執行的部分和子任務。此時就不得不對並行執行的任務加以協調,讓這些任務之間進行彼此通信,以便在它們所完成的作業之間實現同步。
編寫併發程序,主要存在着以下三大基本的挑戰:
1.         確認問題領域的環境中存在的固有並行性;
2.         將軟件適當地分解成兩個或多個任務,這些任務可以在同一時刻執行,即這些任務可以被併發執行;
3.         協調上述過程中所分配的任務,使軟件正確而高效地運行,從而達到預期的目的。
用一句話概括這三點就是:發現並確認、分解、通信和同步。
無論使用哪種併發機制,都會存在着如下幾種障礙:對共享數據的併發訪問,目前提出的解決方案是通過對信號量的P/V原語操作來進行同步,當然也存在着其他同步技術,比如JAVA的多線程就是用一種叫做JAVA監控器的機制來進行同步。
併發同時也引入了一些其他問題,比如:被調用的函數必須具有一種稱爲線程安全的屬性。一個函數被稱爲線程安全的,當且僅當該函數被多個併發線程反覆地調用時,它會一直產生正確的結果。有一類重要的線程安全函數,叫做可重入函數,其特點就是:當這類函數被多個線程調用時,不會引用任何共享數據。可重入性函數是線程安全函數的一個真子集,他們之間的關係如下圖所示:

圖4 可重入函數、線程安全和線程不安全函數之間的集合關係
競爭和死鎖也是併發編程中出現的一類典型問題。當程序員錯誤的假設邏輯流該如何調度時,就會發生競爭,若調度產生錯誤,就有可能發生一個流等待一個永遠不會發生的事件或流,就會產生死鎖。
總結
本文只起一個拋磚引玉的作用,併發編程博大精深,非一兩篇文章所能闡述透徹。由於現行的大多數編程語言都沒有支持並行性的關鍵字(是否所有的語言都不支持,筆者不敢枉下結論),所以需要我們自己在實際的編程過程中摸索前進。但也不需要我們白手起家,業界提供了不少支持併發編程的標準和庫,例如比較流行的有MPICH/PVM/MICO等。最普遍的並行和分佈式編程環境是集羣、MPP和SMP計算機。筆者希望和所有的併發編程愛好者結交朋友,可以通過[email protected]聯繫。並在此非常感謝我MM爲我修改這篇文章,功勞屬於你。
參考文獻
[1] Cameron Hughes ,Tracey Hughes.Parallel and Distributed Programming Using C++
[2] Herb Sutter.The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software
[3] Randal E.Bryant,David O’Hallaron.深入理解計算機系統
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章