導讀:
如果邏輯控制流在時間上重疊,那麼就稱它們是并發(concurrent)的。并發可以看做是一種作業系統核心用來運作多個應用程式的機制,并發不局限于核心。
應用級并發的一些應用場合:(1)通路慢速 I/O 裝置。當一個使用者等待來自慢速 I/O 裝置(比如磁盤)的資料到達時,核心會運作其他程序;(2)與人互動。每次使用者請求某種操作時(比如通過點選滑鼠),一個獨立的并發邏輯流被建立來執行這個操作;(3)通過推遲工作以降低延遲;(4)服務多個網絡用戶端。一個并發伺服器為每個用戶端建立一個單獨的邏輯流;(5)在多核機器上進行并行計算。被劃分稱并發流的應用程式通常在多個機器上比單處理器機器上快很多,因為這些流會并行執行,而不是交錯執行。
現代作業系統提供了三種基本的構造并發程式的方法:(1)程序。在這種形式下,每個邏輯控制流都是一個程序,由核心來排程和維護。因為程序有獨立的虛拟位址空間,想要和其他流通信,控制流必須使用顯式的程序間通信(IPC)機制。(2)I/O 多路複用。在這種形式下,應用程式在一個程序的上下文中顯式地調用它們自己的邏輯流。邏輯流被模型化為狀态機,資料到達檔案描述符後,主程式顯式地從一個狀态轉換到另一個狀态。因為程式是一個單獨的程序,是以所有的流都共享同一個位址空間。(3)線程是運作在一個單一程序上下文中的邏輯流,由核心進行排程。線程像程序流一樣由核心進行排程,像 I/O 多路複用一樣共享同一個虛拟位址空間。
重點解讀:
一、基于程序的并發程式設計
構造并發程式最簡單的方法就是用程序,使用 fork, exec, waitpid 等函數。
優點:父子程序間共享狀态資訊:共享檔案表,但是不共享使用者位址空間。這樣避免了一個程序覆寫另外一個程序的虛拟記憶體。
缺點:獨立的位址空間使得程序共享狀态資訊變得更加困難,需要使用程序間通信(IPC)機制。

二、基于I/O多路複用的并發程式設計
I/O多路複用技術,基本思想就是使用 select 函數,要求核心挂起程序,隻有在一個或多個 I/O 事件發生後,才将控制傳回給應用程式。
IO多路複用可以用到并發事件驅動程式的基礎,在事件驅動程式中某些事件會導緻流向前推進,一般的思路是将邏輯流模型轉化為狀态機,不嚴格的說一個狀态機就是一組狀态、輸入時間和轉移。伺服器使用IO多路複用借助select函數檢測輸入事件的發生,當每個已連接配接描述符準備好可讀時,伺服器就為相應的狀态機執行轉移,在這裡就是從描述符讀和寫回一個文本行。
事件驅動設計的優點:(1)它比基于程序的設計給了程式員更多對程式行為的控制。例如,我們可以編寫一個事件驅動的并發伺服器,為某些用戶端提供它們需要的服務,而對于基于程序的并發伺服器來說是很困難的;(2)一個基于IO多路複用的事件驅動伺服器試運作在單一程序上下文中的,是以每個邏輯流都能通路該程序的全部位址空間,這使得流之間共享資料變得很容易;(3)一個與作為單個程序運作相關的優點是,你可以利用熟悉的調試工具例如GDB,來調試你的并發伺服器,就像對順序程式那樣(4)比基于程序的設計要高效的多,因為他們不需要程序上下文切換來排程新的流。
事件驅動設計的缺點:(1)編碼複雜,上面的事件驅動并發echo伺服器需要的代碼比基于程序的伺服器多三倍,随着并發粒度的減小,複雜性還會上升,這裡的粒度是指每個邏輯流每個時間片執行的指令數量;(2)不能充分利用多核處理器。
三、基于線程的并發程式設計
到目前為止,前面看到了兩種建立并發邏輯流的方法,第一種方法中,每個流使用了單獨的程序,核心會自動排程每個程序,而每個程序有自己的私有位址空間,使流之間共享資料很困難。第二種方法中,建立自己的邏輯流,并利用IO多路複用來顯式地排程流。因為隻有一個程序,所有的流共享整個位址空間,本節介紹第三種方法,基于線程,它是之前地兩種方法的混合。
線程是運作在程序上下文中的邏輯流,在之前程式都是由每個程序中一個線程組成的,現代作業系統允許我們編寫一個程序裡運作多個線程的程式,線程由核心自動排程。每個線程都有自己的線程上下文,包括一個唯一的整數線程ID,棧、棧指針、程式計數器、通用目的寄存器和條件碼,所有運作在一個程序裡的線程共享該程序的整個虛拟位址空間。
基于線程的邏輯流結合了基于程序和基于IO多路複用的流的特征,同程序一樣,線程由核心自動排程,并且核心通過一個整數ID來識别線程。同基于IO多路複用的流一樣,多個線程運作在單一程序的上下文中,共享這個程序虛拟位址空間的所有内容,包括它的代碼、資料、堆、共享庫和打開的檔案。
每個程序開始生命周期時都是單一線程,這個線程被稱為主線程,在某一時刻,主線程建立一個對等線程,從這個時間點開始,兩個線程就并發的運作,最後主線程執行一個慢速系統調用,例如read或者sleep,或者因為被系統的間隔計時器中斷,控制就會通過上下文切換傳遞到對等線程,對等線程會執行一段時間,然後控制傳回主線程,依次類推。
多線程的執行模型在某些方面和多程序的執行模型是相似的。
線程執行在一些方面和程序是不同的,因為一個線程的上下文比一個程序的上下文小得多,線程的上下文切換要比程序快得多。另一個不同的是線程不像程序那樣,不是按照嚴格的父子層次來組織的,和一個程序相關的線程組成一個對等(線程)池,獨立于其他線程建立的線程。主線程和其他線程的差別僅在于它總是程序中第一個運作的線程。對等(線程)池的概念主要影響的是一個線程可以殺死它的任何對等線程或者等待它的任意對等線程終止。另外每個對等線程都能讀寫相同的共享資料。
四、多線程程式中的共享變量
線程存儲器模型:一組并發線程運作在一個程序的上下文中。每個線程都有自己獨立的線程上下文,包括線程ID、棧、棧指針、程式計數器、條件碼和通用目的寄存器值。每個線程和其他線程一起共享程序上下文剩餘部分,其中包括整個使用者虛拟位址空間,它是由隻讀文本(代碼)、讀寫資料、堆以及所有的共享庫代碼和資料區域組成的,線程也共享相同的打開檔案集合。
從實際操作角度來說,讓一個線程去讀寫另一個線程的寄存器值是不可能的,另一方面,任何線程都可以通路共享虛拟記憶體的任意位置,如果某個線程修改了一個記憶體位置,那麼其他每個線程最終都能在它讀這個位置時發現這個變化,是以寄存器是不共享的,而虛拟記憶體總是共享的。
各自獨立的線程棧的記憶體模型不是那麼整齊清楚,這些棧被儲存在虛拟位址空間的棧區域中,并且通常是被相應的線程獨立的通路的,我們說的通常不是總是,因為不同的線程棧是不對其他線程設防的,是以,如果一個線程以某一種方式得到一個指向其他線程棧的指針,那麼它就可以讀寫這個棧的任何部分。
多線程的C程式中變量根據它們的存儲類型被映射到虛拟記憶體。(1)全局變量。全局變量是定義在函數之外的變量,在運作時,虛拟記憶體的讀寫區域隻包含每個全局變量的一個執行個體,任何線程都可以引用。(2)本地自動變量。本地自動變量就是定義在函數内部但是沒有static屬性的變量。在運作時,每個線程的棧都包含它自己的所有本地自動變量的執行個體,即使多個線程執行同一線程例程也是如此。(3)本地靜态變量。本地靜态變量,是定義在函數内部并有static屬性的變量。和全局變量一樣,虛拟記憶體的讀寫區域隻包含在程式中聲明的每個本地靜态變量的一個執行個體。例如,即使每個對等線程都聲明了cnt,在運作時虛拟記憶體的讀寫區域也隻有一個cnt的執行個體,每個對等線程都讀寫這個執行個體。
五、用信号量同步線程
共享變量雖然很友善但是會引入同步錯誤的問題,對于我們而言是沒有辦法預測作業系統是否将為你的線程選擇一個正确的順序,我們需要借助一種叫作進度圖的方法來闡明這些正确或者不正确的指令順序。
信号量提供了一種很友善的方法來確定對共享變量的互斥通路,基本思想是,将每個共享變量與一個信号量s聯系起來,然後用P(s)和V(s)操作将相應的臨界區包圍起來。
除了提供互斥之外,信号量的另一個作用是排程對共享資源的通路,在這種場景中,一個線程用信号量操作來通知另一線程,程式中某個條件已經為真了,兩個經典而有用的例子就是生産者-消費者和讀者-寫者問題。
生産者-消費者問題:
讀者-寫者問題:讀者-寫者問題是互斥問題的一個概括,一組并發的線程要通路一個共享對象,例如,一個主存中的資料結構或者一個磁盤上的資料庫,有的線程隻讀對象,而其他的線程隻修改對象。修改對象的叫作寫者,隻讀對象的線程叫作讀者。寫者必須擁有對對象的獨占的通路,而讀者可以和無線個其他讀者共享對象。一般來說,有無線多個并發的讀者和寫者。
讀寫者問題有幾個變種,分别基于讀者和寫者的優先級。第一類讀者優先,要求不要讓讀者等待,除非已經把使用對象的權限賦予了一個寫者,換言之,讀者不會因為有一個寫者在等待而等待。第二類寫者優先,要求一旦一個寫者準備好可以寫,它就會盡可能盡快完成它的寫操作。同第一類問題不同,在一個寫者後到達的讀者必須等待,即使這個寫者也在等待。
六、使用線程提高并行性
并發(concurrency)和并行(parallellism)是:解釋一:并行是指兩個或者多個事件在同一時刻發生;而并發是指兩個或多個事件在同一時間間隔發生。解釋二:并行是在不同實體上的多個事件,并發是在同一實體上的多個事件。解釋三:并行是在多台處理器上同時處理多個任務。如 hadoop 分布式叢集,并發是在一台處理器上“同時”處理多個任務。
下圖是順序、并發和并行程式之間的幾何關系,所有程式的集合能夠被劃分成不相交的順序集合和并發程式的集合。并行程式是一個運作在多個處理器上的并發程式。
七、并發問題
線程安全:一個函數被稱為線程安全的,當且僅當被多個并發線程反複調用時,它會一直産生正确的結果。如果一個函數不是線程安全的,我們就說它是線程不安全的。
四個(不相交)線程不安全函數(類):(1)不保護共享變量的函數;(2)保持跨越多個調用狀态的函數;(3)傳回指向靜态變量的指針的函數;(4)調用線程不安全函數的函數。
可重入性:有一類重要的線程安全函數,叫作可重入線程安全函數,其特點是,當它們被多個線程調用時,不會引入任何共享資料。盡管線程安全和可重入有時會被當做同義詞但是還是有清晰地技術差别。可重入函數集合是線程安全函數的一個真子集。
競争:當一個程式的正确性依賴于一個線程要在另一個線程到達y點之前到達它的控制流中的x點時,就會發生競争。通常發生競争是因為程式員假定線程将按照某種特殊的軌迹線穿過執行狀态空間,而忘記另一條準則規定:多線程的程式必須對任何可行的軌迹線都正确工作。
死鎖:信号量引入了一個潛在的運作錯誤,叫作死鎖。它指的是一組線程被阻塞了,等待一個永遠不會為真的條件。進度圖對于了解死鎖是一個很好的工具。下圖展示了一對用兩個信号量實作互斥的線程的進度圖(它是指一組線程被阻塞了,等待一個永遠也不會為真的條件)
程式員使用P和V操作順序不當,以至于兩個信号量的禁止區域重疊,如果某個執行軌迹線碰巧到達了死鎖狀态d,那麼就不可能有進一步的進展了,因為重疊的禁止區域阻塞了每個合法向上的進展,換句話說,程式死鎖是因為每個線程都在等待其他線程執行一個根本不可能發生的V操作。
重疊的禁止區域引起了一組稱為死鎖區域的狀态,如果一個軌迹線碰巧到達了一個死鎖區域的狀态,那麼死鎖就不能避免了,軌迹線可以進入死鎖區域,但不可能離開。
避免死鎖:明确互斥鎖加鎖的順序規則,給所有的互斥操作的一個全序,如果每個線程都是以一種順序獲得互斥鎖并以相反的順序釋放,那麼這個程式就是無鎖。