深入理解计算机系统结构——并发编程
并發編程
如果邏輯控制流在實際上重疊,那么它們就是并發的,這種常見的現象稱為并發,出現在計算機系統的許多不同層面上。
應用級并發在其他情況下也是很有用的:
- 訪問慢速I/O設備。
- 與人交互。
- 通過推遲工作以降低延遲。
- 服務多個網絡客戶端。
- 在多核機器上進行并行計算。
使用應用級并發的應用程序稱為并發程序。現代操作系統提供了三種基本的構造并發程序的方法:
- 進程。用這種方法,每個邏輯控制流都是一個進程,由內核來調度和維護。因為進程有獨立的虛擬地址空間,想要和其他流通信,控制流必須使用某種顯式的進程間通信機制。
- I/O多路復用。在這種形式的并發編程中,應用程序在一個進程的上下文中顯式地調度它們自己的邏輯流。邏輯流被模型化為狀態機,數據到達文件描述符后,主程序顯式地從一個狀態轉換到另一個狀態。因為程序是一個單進程,所以所有的流都共享同一地址空間。
- 線程。線程是運行在一個單一進程上下文中的邏輯流,由內核進行調度。你可以把線程看成是其他兩種方式的混合體,像進程流一樣由內核進行調度,而像I/O多路復用流一樣共享同一虛擬地址空間。
基于進程的并發編程
構造并發編程最簡單的方法就是用進程,使用那些大家都很熟悉的函數,像fork、exec和waitpid。
步驟:
1)服務器監聽一個監聽描述符上的連接請求。
2)服務器接受了客戶端1的連接請求,并返回一個已連接描述符。
3)在接受了連接請求之后,服務器派生一個子進程,這個子進程獲得服務器描述符表的完整拷貝。子進程關閉它的拷貝中的監聽描述符3,而父進程關閉它的已連接描述符4的拷貝,因為不再需要這些描述符了。
4)子進程正忙于為客戶端提供服務,父進程繼續監聽新的請求。
注意:子進程關閉監聽描述符和父進程關閉已連接描述符是很重要的,因為父子進程共用同一文件表,文件表中的引用計數會增加,只有當引用計數減為0時,文件描述符才會真正關閉。所以,如果父子進程不關閉不用的描述符,將永遠不會釋放這些描述符,最終將引起存儲器泄漏而最終消耗盡可以的存儲器,是系統崩潰。
????????????????
使用進程并發編程要注意的問題:
1)首先,通常服務器會運行很長的時間,所以我們必須要包括一個SIGCHLD處理程序,來回收僵死子進程的資源。因為當SIGCHLD處理程序執行時,SIGCHLD信號時阻塞的,而Unix信號時不排隊的,所以SIGCHLD處理程序必須準備好回收多個僵死子進程的資源。
2)其次,子進程必須關閉它們各自的connfd拷貝。就像我們已經提到過的,這對父進程而言尤為重要,它必須關閉它的已連接描述符,以避免存儲器泄漏。
3)最后,因為套接字的文件表表項中的引用計數,直到父子進程的connfd都關閉了,到客戶端的連接才會終止。
缺點:
對于父子進程間共享狀態信息,進程有一個非常清晰的模型:共享文件表,但是不共享用戶地址空間。進程有獨立的地址空間既是優點也是缺點。這樣一來,一個進程不可能不小心覆蓋另一個進程的虛擬存儲器,這就消除了許多令人迷惑的錯誤——這是一個明顯的優點。
另一方面,獨立的地址空間使得進程共享狀態信息變得更加困難。為了共享信息,它們必須使用顯式的IPC(進程間通信)機制。基于進程的設計的另一個缺點是,它們往往比較慢,因為進程控制和IPC的開銷很高。
基于I/O多路復用的并發編程
? ? 1、面對困境——服務器必須響應兩個互相獨立的I/O事件:1)網絡客戶端發起的連接請求? 2)用戶在鍵盤上鍵入的命令 ,解決的辦法是I/O多路復用技術。基本思想是,使用select函數,要求內核掛起進程,只有在一個或多個I/O事件發生后,才將控制返回給應用程序。
可以使用select、poll和epoll來實現I/O復用。
I/O多路復用技術的優劣:
1)使用事件驅動編程,這樣比基于進程的設計給了程序更多的對程序行為的控制。
2)一個基于I/O多路復用的事件驅動服務器是運行在單一進程上下文中的,因此每個邏輯流都訪問該進程的全部地址空間。這使得在流之間共享數據變得很容易。一個與作為單進程運行相關的優點是,你可以利用熟悉的調試工具,例如GDB來調試你的并發服務器,就像對順序程序那樣。最后,事件驅動設計常常比基于進程的設計要高效很多,因為它們不需要進程上下文切換來調度新的流。
缺點:
事件驅動設計的一個明星的缺點就是編碼復雜。我們的事件驅動的并發服務器需要比基于進程的多三倍。不幸的是,隨著并發粒度的減小,復雜性還會上升。這里的粒度是指每個邏輯流每個時間片執行的指令數量。
基于事件的設計的另一重大的缺點是它們不能充分利用多核處理器。
基于線程的并發編程
在使用進程并發編程中,我們為每個流使用了單獨的進程。內核會自動調用每個進程。每個進程有它自己的私有地址空間,這使得流共享數據很困難。在使用I/O多路復用的并發編程中,我們創建了自己的邏輯流,并利用I/O多路復用來顯式地調度流。因為只有一個進程,所有的流共享整個地址空間。而基于線程的方法,是這兩種方法的混合。
線程就是運行在進程上下文的邏輯流。線程由內核自動調度。每個線程都有它自己的線程上下文,包括一個唯一的整數線程ID、棧、棧指針、程序計數器、通用目的寄存器和條件碼。所有的運行在一個進程里的線程共享該進程的整個虛擬地址空間。
基于線程的邏輯流結合了基于線程和基于I/O多路復用的流的特性。同進程一樣,線程由內核自動調度,并且內核通過一個整數ID來標識線程。同基于I/O多路復用的流一樣,多個線程運行在單一進程的上下文中,因此共享這個線程虛擬地址空間的整個內容,包括它的代碼、數據、堆、共享庫和打開的文件。
線程執行模型
多線程的執行模型在某些方面和多進程的執行模型是相似的。每個進程開始生命周期時都是單一線程,這個線程是主線程。在某一時刻,主線程創建一個對等線程,從這個時間點開始,兩個線程就并發地運行。最后,因為主線程執行一個慢速系統調用,例如read和sleep,或者因為它被系統的間隔計時器中斷,控制就會通過上下文切換到對等線程。對等線程會執行一段時間,然后控制傳遞回主線程,依次類推。
在一些重要的方法,線程執行時不同于進程的。因為一個線程的上下文要比一個進程的上下文小很多,線程的上下文切換要比進程的上下文切換快得多。另一個不同就是線程不像進程那樣,不是按照嚴格的父子層次來組織的。和一個進程相關的線程組成一個對等(線程)池,獨立于其他線程創建的線程。主線程和其他線程的區別僅在于它總是進程中第一個運行的線程。對等(線程)池概念的主要影響是,一個線程可以殺死它的任何對等線程,或者等待它的任意對等線程終止。另外,每個對等線程都能讀寫相同的共享數據。
?
Posix 線程
創建線程
線程通過調用pthread_create函數來創建其他線程:
pthread_create函數創建一個新的線程,并帶著一個輸入變量arg,在新線程的上下文中運行線程例程f。能用attr參數來改變新創建線程的默認屬性。
當pthread_create返回時,參數tid包含新創建線程的ID。新線程可以通過調用pthread_self函數來獲得它自己的線程ID。
終止線程
一個線程是以下列方式之一來終止的:
- 當頂層的線程例程返回時,線程會隱式地終止
- 通過調用pthread_exit函數,線程會顯式地終止。如果主線程調用pthread_exit,它會等待所有其他對等線程終止,然后再終止主線程和這個進程,返回值為thread_return。
- ?某個對等線程調用exit函數,則函數終止進程和所有與該進程相關的線程;
- 另一個對等線程調用以當前ID為參數的函數ptherad_cancel來終止當前線程。
?
回收已終止線程的資源
???? pthread_join函數會終止,直到線程tid終止,將線程例程返回的(void*)指針賦值為thread_return指向的位置,然后回收已終止線程占用的所有存儲器資源。和wait不同,該函數只能回收指定id的線程,不能回收任意線程。
?
分離線程
在任何一個時間點上,線程是可結合的或者是分離的。一個可結合的線程能夠被其他線程收回其資源和殺死。在被其他線程回收之前,它的存儲器資源(例如棧)式沒有被釋放的。相反,一個分離的線程是不能被其他線程回收和殺死的。它的存儲器資源在它終止時由系統自動釋放。
默認情況下,線程被創建成可結合的。為了避免存儲器泄漏,每個可結合線程都應該要么被其他線程顯式地收回,要么通過調用pthread_detach函數被分離。
pthread_detach函數分離可結合線程tid。線程能夠通過以pthread_self()為參數的pthread_detach調用來分離它們自己。
? ? ?初始化線程:該函數用來初始化多個線程共享的全局變量。
?????????????????
多線程程序中的共享變量
從一個程序員的角度來看,線程很有吸引力的一個方面就是多個線程很容易共享相同的程序變量。然而,這種共享也是很棘手的。為了編寫正確的線程化程序,我們必須對所謂的共享以及它是如何工作的有很清楚的了解。
為了理解變量是否是共享的,有一些基本的問題要解答:1)線程的基礎存儲器模型是什么?2)根據這個模型,變量實例是如何映射到存儲器的?3)最后,有多少線程引用這些實例?一個變量是共享的,當且僅當多個線程引用這個變量的某個實例。
線程存儲器模型:
一組并發線程運行在一個進程的上下文中。? 每個線程都有它自己獨自的線程上下文,包括線程ID、棧、棧指針、程序計數器、條件碼和通用目的寄存器值。每個線程和其他線程一起共享進程上下文的剩余部分。這包括整個用戶虛擬地址空間,它是由只讀文本(代碼)、讀/寫數據、堆以及所有的共享庫代碼和數據區域組成的。線程也共享同樣的打開文件的集合。
從實際操作的角度來說,讓一個線程去讀或寫另一個線程的寄存器值時不可能的。另一方面,任何線程都可以訪問共享虛擬存儲器的任意位置。如果某個線程修改了存儲器的位置,那么其他每個線程最終都能在它讀這個位置時發現這個變化。因此,寄存器是從來不共享的,而虛擬存儲器總是共享的。
各自獨立的線程棧的存儲器模型不是那么整齊清楚的。這些棧被保存在虛擬地址空間的棧區域中,并且通常是被相應的線程獨立地訪問的。我們說通常而不是總是,是因為不同的線程棧是不對其他線程設防的。所以,如果一個線程以某種方式得到一個指向其他線程棧的指針,那么它就可以讀寫這個棧的任何部分。
將變量映射到存儲器:
線程化的C程序中變量根據它們的存儲器類型被映射到虛擬存儲器:
- 全局變量。全局變量是定義在函數之外的變量。在運行時,虛擬存儲器的讀/寫區域只包含每個全局變量的一個實例,任何線程都可以引用。
- 本地自動變量。本地自動變量就是定義在函數內部但是沒有static屬性的變量。在運行時,每個線程的棧都包含它自己的所有本地自動變量的實例。即使當多個線程執行同一個線程例程時也是如此。
- 本地靜態變量。本地靜態變量是定義在函數內部并有static屬性的變量。和全局變量一樣,虛擬存儲器的讀/寫區域只包含在程序中聲明的每個本地靜態變量的一個實例。
共享變量
我們說一個變量v是共享的,當且僅當它的一個實例被一個以上的線程引用。
共享變量的同步與互斥
1)使用信號量同步線程
??????? 共享變量引入了同步錯誤。
??????? 進度圖:?????????????????????????????????????????????????????????????????????????????????????????? 軌跡線示例:??????????????????????????????????????????????????????????????????? 臨界區(不安全區):
???????????????????
???? 信號量:是用信號量解決同步問題,信號量s是具有非負整數值的全局變量,有兩種特殊的操作來處理(P和V):
??????????????? P(s):如果s非零,那么P將s減1,并且立即返回。如果s為0,那么就掛起這個線程,直到s變為非零;
??????????????? V(s):V操作將s加1。
??? 使用信號量實現互斥:
???????????????????????????????? ????
????? 利用信號量調度共享資源:在這種場景中,一個線程用信號量操作來通知另一個線程,程序狀態中的某個條件已經為真了。兩個經典應用:
????? a)生產者——消費者問題
??????????????????????????????????
????? 要求:必須保證對緩沖區的訪問是互斥的;還需要調度對緩沖區的訪問,即,如果緩沖區是滿的(沒有空的槽位),那么生產者必須等待直到有一個空的槽位為止,如果緩沖區是空的(即沒有可取的項目),那么消費者必須等待直到有一個項目變為可用。
????????????????????????????????????? ??????
??????????????????????????????????????????
??? ????????????????????????????????????
?????? 注釋:5~13行,緩沖區初始化,主要是對緩沖區結構體進行相關操作;16~19行,釋放緩沖區存儲空間;22~29行,生產(有空槽的話,在空槽中插入內容);32~4行,消費(去除某個槽中的內容,使該槽為空)
????? b)讀者——寫者問題
??????? 修改對象的線程叫做寫者;只讀對象的線程叫做讀者。寫著必須擁有對對象的獨占訪問,而讀者可以和無限多個其他讀者共享對象。讀者——寫者問題基本分為兩類:第一類,讀者優先,要求不要讓讀者等待,除非已經把使用對象的權限賦予了一個寫者。換句話說,讀者不會因為有一個寫者等待而等待;第二類,寫者優先,要求一定能寫者準備好可以寫,它就會盡可能地完成它的寫操作。同第一類問題不同,在一個寫者后到達的讀者必須等待,即使這個寫者也是在等待。以下程序給出了第一類讀者——寫者問題的解答:
??????????????????????????????????????????
????? 注釋:信號量w控制對訪問共享對象的臨界區的訪問。信號量mutex保護對共享變量readcnt的訪問,readcnt統計當前臨界區的讀者數量。每當一個寫者進入臨界區,它就對互斥鎖w加鎖,每當它離開臨界區時,對w解鎖,這就保證了任意時刻臨界區最多有一個寫者;另一方面,只有第一個進入臨界區的讀者對w加鎖,而只有最后一個離開臨界區的讀者對w解鎖。
????? 綜合:基于預線程的并發服務器之前介紹的基于線程的并發服務器,需要為每個客戶端新建一個新線程,導致不小的代價。一個基于預線程化的服務器通過使用如下圖所示的生產者——消費者模型來降低這種開銷。服務器是由一個主線程和一組工作組線程構成的。主線程不斷地接受來自客戶端的連接請求,并將得到的連接描述符放在一個有限緩沖區中。每一個工作組線程反復地從共享緩沖區中取出描述符,為客戶端服務,然后等待下一個描述符。
???????????????????????????????????
????? 程序示例如下圖:
???????????????????????????????????????????????????????????????
???????????????????????????????????????????????????????????????
???????????????????????????????????????????????????????????????
?????????????????????????????????????????????????????????????
????????????????????????????????????????????????????????????
?????? 注釋:26~27行,產生工作組線程;29~32行,接受客戶端的連接請求,并把這些描述符放到緩沖區;35~43行,每個線程所要完成的工作;19行,初始化線程共享的全局變量。初始化有兩種方式,一種是它要求主線程顯示地調用一個初始化函數;第二種是,在此顯示的,當第一次有某個線程調用echo_cnt函數時,使用pthread_once函數去調用初始化函數。
其他并發問題
1)線程安全
當用線程編寫程序時,我們必須小心地編寫那些具有稱為線程安全性屬性的函數。一個函數被稱為線程安全的,當且僅當被多個并發線程反復地調用時,它會一直產生正確的結果。如果一個函數不是線程安全的,我們就說它是線程不安全的。
我們能夠定義出四個(不想交的)線程不安全函數類:
第一類:不保護共享變量的函數。
第二類:保持跨越多個調用的狀態的函數。一個偽隨機數生成器是這類線程不安全函數的簡單例子。rand函數是線程不安全的,因為檔期調用的結果依賴于前次調用的中間結果。當調用srand為rand設置了一個終止后,我們從一個但線程中反復地調用rand,能夠預期得到一個可重復的隨機數字序列。
第三類:返回指向靜態變量的指針的函數。某些函數,例如ctime和gethostbyname,將計算結果放在一個static變量中,然后返回一個指向這個變量的指針。如果我們從并發線程中調用這些函數,那么將可能發生災難,因為正在被一個線程使用的結果會被另一個線程悄悄地覆蓋了。
有兩種方法來處理這類線程不安全函數。一種選擇是重寫函數,使得調用者傳遞存放結果的變量的地址。這就消除了所有共享數據,但是它要求程序員能夠修改函數的源代碼。
如果線程不安全是難以修改或不可能修改的,那么另外一種選擇是使用加鎖-拷貝技術。基本思想是將線程不安全函數與互斥鎖聯系起來,在每一個調用位置,對互斥鎖加鎖,調用線程不安全函數,將函數返回的結果拷貝到一個私有的存儲器位置,然后對互斥鎖解鎖。為了盡可能減少對調用者的修改,你應該定義一個線程安全的包裝函數,它執行加鎖-拷貝,然后通過調用這個包裝函數來取代對線程不安全函數的調用。
第四類:調用線程不安全函數的函數。如果函數f調用線程不安全函數g,那么f就是線程不安全的嗎?不一定。如果g是第二類資源,即依賴于跨越多次調用的狀態,那么f也是線程不安全的,而且除了重寫g以為,沒有辦法。然而,如果g是第一類或第三類函數,那么只要你用一個互斥鎖保護調用位置和任何得到的共享數據,f仍然可能是線程安全的。
2)可重入性
有一類重要的線程安全函數,叫做可重入函數,其特點在于它們具有這樣一種屬性:當它們被多個線程調用時,不會引用共享數據。盡管線程安全和可重入有時會被用做同義詞,但是它們之間還是有清晰的技術差別的。下圖表示了可重入函數、線程安全函數和線程不安全函數之間的集合關系。可重入函數集合是線程安全函數的一個真子集。
可重入函數通常比不可重入函數高效一些,因為不需要同步操作。
如果所有的函數參數都是傳值傳遞(沒有指針),且所有的數據引用都是本地的自動棧變量(沒有引用靜態或全局變量),則函數是顯式可重入的,無論如何調用,都沒有問題。
允許顯式可重入函數中部分參數用指針傳遞,則隱式可重入的。在調用線程時小心傳遞指向非共享數據的指針,它才是可重入。如rand_r。
可重入性同時是調用者和被調用者的屬性。
3)競爭
當一個程序的正確性依賴于一個線程要在另一個線程到達y點之前到達它的控制流中的x點時,就會發生競爭。
4)死鎖
信號量引入了一種潛在的令人厭惡的運行時錯誤,叫做死鎖,它指的是一組線程被阻塞了,等待一個永遠也不會為真的條件。
避免死鎖是很困難的。當使用二進制信號量來實現互斥時,可以用如下規則避免:
如果用于程序中每對互斥鎖(s,t),每個既包含s也包含t的線程都按照相同順序同時對它們加鎖,則程序是無死鎖的。
轉載于:https://www.cnblogs.com/wuchanming/p/4461005.html
總結
以上是生活随笔為你收集整理的深入理解计算机系统结构——并发编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: []==![]为true
- 下一篇: IIS 8.5配置.net网站[花了半个