第12章并發程式設計
12.1 基于程序的并發程式設計
12.1.1 基于程序的并發伺服器
基于程序的并發的echo伺服器的代碼的重要說明:
·首先,通常伺服器會運作很長時間,是以我們必須包括一個SIGCHLD處理程式,來回收僵死子程序資源。
·其次,父子程序必須關閉他們的connfd拷貝。
· 最後,因為套接字的檔案表表項的引用計數,直到父子程序的connfd都關閉了,到用戶端的連接配接才會終止。
12.1.2關于程序的優劣
對于在父、子程序間共享狀态資訊,程序有一個非常清晰的模型:共享檔案表,但是不共享使用者位址空間。程序有獨立的位址控件愛你既是優點又是缺點。由于獨立的位址空間,是以程序不會覆寫另一個程序的虛拟存儲器。但是另一方面程序間通信就比較麻煩,至少開銷很高。
12.2 基于i/o多路複用的并發程式設計
- 伺服器必須響應兩個互相獨立的I/O事件:
1)網絡用戶端發起連接配接請求
2) 使用者在鍵盤上鍵入指令行
2. 針對這種困境的一個解決辦法就是I/O多路複用技術。基本思路就是使用select函數,要求核心挂起程序,隻有在一個或者多個I/O事件發生後,才将控制返給應用程式。
12.2.1基于i/o多路複用的并發事件驅動伺服器
I/O多路複用可以用做并發事件驅動程式的基礎,在事件驅動程式中,流是因為某種事件而前進的,一般概念是将邏輯流模型化為狀态機,不嚴格地說,一個狀态機就是一組狀态,輸入事件和轉移,其中轉移就是将狀态和輸入事件映射到狀态,每個轉移都将一個(輸入狀态,輸入事件)對映射到一個輸出狀态,自循環是同一輸入和輸出狀态之間的轉移,通常把狀态機畫成有向圖,其中節點表示狀态,有向弧表示轉移,而弧上的标号表示輸人事件,一個狀态機從某種初始狀态開始執行,每個輸入事件都會引發一個從目前狀态到下一狀态的轉移。
12.2.2i/o多路複用技術的優劣
- 事件驅動設計的一個優點是,它比基于程序的設計給了程式員更多的對程式行為的控制。例如我們可以設想編寫一個事件驅動的并發伺服器,為某些客戶提供他們需要的服務,而這對于新程序的并發伺服器來說,是很困難的
- 另一個優點是,一個基于I/O多路複用的事件驅動器是運作在單一程序上下文中的,是以每個邏輯流都能通路該程序的全部位址空間。這使得在流之間共享資料變得很容易,一個與作為單個程序運作相關的優點是,你可以利用熟悉的調試工具,例如GDB,來調試你的并發伺服器,就像對順序程式那樣。最後,事件驅動設計常常比基于進利的設計要高效得多,因為它們不需要程序上下文切換來排程新的流。
· 事件驅動設計的一個明顯的缺點就是編碼複雜,我們的事件驅動的并發伺服器需要的代度是指每個邏輯流每個時間片執行的指令數量。基于事件的設計的另一個重大缺點是它們不能充分利利用多核處理器。
12.3 基于線程的并發程式設計
每個線程都有自己的線程上下文,包括一個線程ID、棧、棧指針、程式計數器、通用目的寄存器和條件碼。所有的運作在一個程序裡的線程共享該程序的整個虛拟位址空間。由于線程運作在單一程序中,是以共享這個程序虛拟位址空間的整個内容,包括它的代碼、資料、堆、共享庫和打開的檔案。
12.3.1 線程執行模型
- 線程執行的模型。線程和程序的執行模型有些相似。每個程序的聲明周期都是一個線程,這個線程稱之為主線程。但是大家要有意識:線程是對等的,主線程跟其他線程的差別就是它先執行。
12.3.2 Posix線程
- Posix線程是在C程式中處理線程的一個标準接口。它最早出現在1995年,而且在大多數Unix系統上都可用。Pthreads定義了大約60個函數,允許程式建立、殺死和回收線程,與對等線程安全地共享資料,還可以通知對等線程系統狀态的變化。
12.3.3 建立線程
- 線程通過調用pthread_create函數來建立其他程序。
12.3.4 終止線程
- 一個線程是以下列方式之一來終止的。
· 當頂層的線程例程傳回時,線程會隐式地終止
· 通過調用pthread_exit函數,線程會顯它會等待所有其他對等線程終止,然後再終止式地終止。
- 某個對等線程調用Unix的exit函數,該函數終止程序以及所有與該程序相關的線程
- 另一個對等線程通過以目前線程ID作為參數調用pthread_cancle函數來終止目前線程
12.3.5 回收已終止線程的資源
12.3.6 分離線程
12.3.7 初始化線程
12.3.8 一個基于線程的并發伺服器
12.4 多線程程式中的共享變量
12.4.1線程存儲器模型
·一組并發線程運作在一個程序的上下文中。每個線程都有它自己獨立的線程上下文,包括線程ID、棧、棧指針、程式計數器、條件碼和通用目的寄存器值。每個線程和其他線程一起共享程序上下文的剩餘部分。這包括整個使用者虛拟位址空間,它是由隻讀文本代碼、讀/寫資料、堆以及所有的共享庫代碼和資料區域組成的。線程也共享同樣的打開檔案的集合。
·從實際操作的角度來說,讓一個線程去讀或寫另一個線程的寄存器值是不可能的。另一方面,任何線程都可以通路共享虛拟存儲器的任意位置。如果某個線程修改了一個存儲器位置,那麼其他每個線程最終都能在它讀這個位置時發現這個變化。是以,寄存器是從不共享的,而虛拟存儲器總是共享的。
·各自獨立的線程棧的存儲器模型不是那麼整齊清楚的。這些棧被儲存在虛拟位址空間的棧區域中,并且通常是被相應的線程獨立地通路的。我們說通常而不是總是,是因為不同的線程棧是不對其他線程設防的是以,如果個線程以某種方式得到個指向其他線程棧的指慧:那麼它就可以讀寫這個棧的任何部分。
12.4.2 将變量映射到存儲器
線程化的C程式中變量根據它們的存儲類型被映射到虛拟存儲器:
·全局變量
·本地自動變量
·本地靜态變量
12.4.3 共享變量
我們說一個變量V是共享的,當且僅當它的一個執行個體被一個以上的線程引用。例如,示例程式中的變量cnt就是共享的,因為它隻有一個運作時執行個體,并且這個執行個體被兩個對等線程引用在另一方面,myid不是共享的,因為它的兩個執行個體中每一個都隻被一個線程引用。然而,認識到像msgs這樣的本地自動變量也能被共享是很重要的。
12.5 用信号量同步線程
- 信号量通常稱之為PV操作,雖然它的思想是将臨界代碼保護起來,達到互斥效果。這裡面作業系統使用到了線程挂起。
- 将線程i的循環代碼分解成五個部分:
12.5.1 進度圖
- 程序圖将n個并發程序的執行模型化為一條n維笛卡爾空間中的軌迹線。
12.5.2 信号量
- 信号量s是具有非負整數值的全局變量,隻能由兩種特殊的操作來處理,這兩種操作稱為P和V
- P(s):如果s是非零的,那麼P将s減1并且立即傳回。如果s為零,那麼就挂起這個線程,直到s變為非零,而一個y操作會重新開機這個線程。在重新開機之後,P操作将s減1并将控制傳回給調用者
- V(s):V操作将s加1。如果有任何線程阻塞在P操作等待s變成非零,那麼V操作會重新開機這些線程中的一個,然後該線程将s減1,完成它的P操作,P中的測試和減1操作是不可分割的,也就是說,一旦預測信号量s變為非零,就會将s減1,不能有中斷。V中的加1操作也是不可分割的,也就是加載、加和存儲信号量的過程中沒有中斷。注意,V的定義中沒有定義等待線程被重新啟動的順序。唯—的要求是V必須隻能重新開機一個正在等待的程序。
12.5.3 使用信号量來實作互斥
- 信号量提供了一種很友善的方法來確定對共享變量的互斥通路。基本思想是将每個共享變量(或者一組相關的共享變量)與一個信号量聯系起來 。以這種方式來保護共享變量的信号量叫做二進制信号量,因為它的值總是0或者1。以提供互斥為目的的二進制信号量常常也稱為互斥鎖。在一個互斥鎖上執行P操作稱為對互斥鎖加鎖。類似地,執行V操作稱為對互斥鎖解鎖。對一個互斥鎖加了鎖但是還沒有解鎖的線程稱為占用這個互斥鎖。一個被用作一組可用資源的計數器的信号量稱為計數信号量。關鍵思想是這種P和V操作的結合建立了一組狀态,叫做禁止區。因為信号量的不變性,沒有實際可行的軌迹線能夠包含禁止區中的狀态。而且,因為禁止區完全包括了不安全區,是以沒有實際可行的軌迹線能夠接觸不安全區的任何部分。是以,每條實際可行的軌迹線都是安全的,而且不管運作時指令順序是怎樣的,程式都會正确地增加計數器的值。
12.5.4利用信号量來排程共享資源
1.生産者-消費者問題。
2.讀者-寫者問題。
12.5.5 綜合:基于預線程化的并發伺服器
12.6 使用線程提高并行性
到目前為止,在對并發的研究中,我們都假設并發線程是在單處許多現代機器具有多核處理器。并發程式通常在這樣的機器上運理器系統上執行的。然而,在多個核上并行地排程這些并發線程,而不是在單個核順序地排程,在像繁忙的Web伺服器、資料庫伺服器和大型科學計算代碼這樣的應用中利用這種并行性是至關重要的。
12.7 其他并發問題
12.7.1 線程安全
- 我們程式設計過程中,盡可能編寫線程安全函數,即一個函數當且僅當被多個并發線程反複調用時,它會一直産生正确的結果。如果做不到這個條件我們稱之為線程不安全函數。下面介紹四類線程不安全函數:
●不保護共享變量的函數。解決辦法是PV操作。
●保持跨越多個調用的狀态函數。比如使用靜态變量的函數。解決方法是不要使用靜态變量或者使用可讀靜态變量。
●傳回指向靜态變量的指針的函數。解決方法是lock-and-copy(枷鎖-拷貝)
●調用線程不安全函數的函數
死鎖。
- 由于PV操作不當,可能造成死鎖現象。這在程式中也會出現。
12.7.2 可重入性
- 有一類重要的線程安全函數,叫做可重入函數。其特點在于他們具有這樣一種屬性:當它們被多個線程調用時,不會引用任何共享資料。盡管線程安全和可重入有時會(正确地)被用做同義詞,但是它們之間還是有清晰的技術差别的,值得留意。圖展示了可重入函數、線程安全函數和線程不安全函數之間的集合關系。所有函數的集合被劃分成不相交的線程安全和線程不安全函數集合。可重入函數集合是線程安全函數的一個真子集。
- 可重入函數通常要比不可重入的線程安全的函數高效一些,因為它們不需要同步操作。更進一步來說,将第2類線程不安全函數轉化為線程安全函數的唯一方法就是重寫它,使之變為可重入的。
12.7.3 線上程化的程式中使用已存在的庫函數
12.7.4 競争
- 當一個程式的正确性依賴于一個線程要在另一個線程到達y點之前到達它的控制流中的X點時,就會發生競争。通常發生競争是因為程式員假定線程将按照某種特殊的軌迹正确工作忘記了另一條準則規定:線程化的程式必須對任何可行的軌迹線都正确工作。
12.7.5 死鎖
- 信号量引入了一種潛在的令人厭惡的運作時錯誤,叫做死鎖。它指的是一組線程被阻塞了,等待一個永遠也不會為真的條件。進度圖對于了解死鎖是一個無價的工具。
- 關于死鎖的重要知識:
- 程式員使用P和V操作漏序不當,以至于兩個信号量的禁止區域重疊。如果某個執行軌迹線碰巧到達了死鎖狀态d那麼就不可能有進一步的進展了,因為重疊的禁止區域阻塞了每個合法方向上的進展。換句話說,程式死鎖是因為每個線程在等待一個根本不可能發生的V操作
- 重疊的禁止區域引起了一組稱為死鎖區域的狀态。軌迹線可以進入死鎖區域,但是它們不可能離開。
- 死鎖是個相當困難的問題,因為它不總是可預測的。一些幸運的執行軌迹線将繞開死鎖區域,而其他的将會陷入這個區域。
12.7 小結
一個并發程是由在時間上重疊的一組邏輯流組成的。在這一章中,我們學習了三種不同的應用程式程、I/O多路複用和線程。我們以一個并發網絡伺服器作為貫穿全章的應用程式。
程序是由核心自動排程的,而且因為它們有各自獨立的虛拟位址空間,是以要實作共享資料,必須要有顯式的IPC機制。事件驅動程式建立它們自己的并發邏輯流,這些邏輯流被模型之間共享資料速度很快而且很容多路複用來顯式地排程這些流。
無論哪種并發機制,同步對共享資料的并發通路都是一個困難的問題。提出對信号量的P和V操作就是為了幫助解決這個問題。信号量操作可以用來提供對共享資料的互斥通路,也對諸如生産者-消費者程式中有限緩沖區和讀者―寫者系統中的共享對象這樣的資源通路進行排程。
并發也引入了其他一些困難的問題。被線程調用的函數必須具有一種稱為線程安全的屬性。我們定義了四類線程不安全的函數,以及一些将它們變為線程安全的建議。可重入函數是線程安全函數的一個真子集,它不通路任何共享資料。可重入函數通常比不可重入函數更為有效,因為它們不需要任何同步原語。競争和死鎖是并發程式中出現的另一些困難的問題。當程式員錯誤地假設邏輯流該如何排程時,就會發生競争。當一個流等待一個永遠不會發生的事件時,就會産生死鎖。
轉載于:https://www.cnblogs.com/Juliet5307/p/5017370.html