如果邏輯控制流在時間上是堆疊,那末它們就是并發(fā)的(concurrent)
。這類常見的現(xiàn)象稱為并發(fā)(concurrency)
。
我們主要將并發(fā)
看作是1種操作系統(tǒng)內(nèi)核用來運行多個利用程序的機制。
但是,并發(fā)
不單單局限于內(nèi)核。它也能夠在利用程序中扮演重要的角色。
例如
Unix
信號處理程序如何允許利用響應(yīng)異步事件 ctrl-c
其他情況
訪問慢速I/O
裝備
I/O
裝備(例如磁盤)的數(shù)據(jù)到達時,內(nèi)核會運行其他進程,使CPU保持繁忙。與人交互
通過推延工作以下降延遲
并發(fā)
來下降某些操作的延遲使用利用級并發(fā)的利用程序稱為并發(fā)程序(concurrent program)
.
操作系統(tǒng)提供3種基本的構(gòu)造并發(fā)程序的方法:
進程
每一個邏輯控制流 都是1個進程
由于進程
有獨立的虛擬地址空間
通訊
,控制流必須使用某種顯式的進程間通訊(interprocess communication,IPC)
進制I/O
多路復(fù)用(暫時不太懂) 邏輯流
。邏輯流
被模型化為狀態(tài)機
,數(shù)據(jù)到達文件描寫符后,主程序顯式地從1個狀態(tài)轉(zhuǎn)換到另外一個狀態(tài)。線程
是運行在1個單1進程上下文中的邏輯流
,有內(nèi)核調(diào)度。 進程
1樣由內(nèi)核進行調(diào)度。I/O
多路復(fù)用1流1樣同享1個虛擬地址空間。1個構(gòu)造并發(fā)服務(wù)器的自然方法就是,在父進程中接收客戶端連接要求,然后創(chuàng)建1個新的子進程來為每一個新客戶端提供服務(wù)。
用Signal(SIGCHLD,sigchld_handler)
回收僵死進程。
8.5.7
28
行,33
行 父子進程各自關(guān)閉他們不需要的拷貝。
由于文件表項的援用計數(shù),直到父進程關(guān)閉它的描寫符,才算結(jié)束1次連接
對在父,子進程間同享狀態(tài)信息,進程有1個非常清晰的模型
。
進程
具有獨立的虛擬地址空間即是 優(yōu)點,也是 缺點。
優(yōu)點
:1個進程
不可能不謹慎覆蓋另外一個進程的虛擬存儲空間。
缺點
:獨立的地址空間使得進程間同享信息也很困難。
必須使用顯式的IPC
(進程間通訊)機制。
常常還比較慢
IPC
的開消都很大。假定要編寫1個echo服務(wù)器
。
服務(wù)器
既能響應(yīng)客戶端
的要求exit
).因此,服務(wù)器
必須要響應(yīng)兩個相互獨立的I/O
事件
不管先等待那個事件都不是理想的,解決辦法之1是就是使用I/O多路復(fù)用技術(shù)
。
select
函數(shù),要求內(nèi)核掛起進程,只有1個或多個I/O
事件產(chǎn)生后,才將控制返回給利用程序。線程(thread)
就是運行在進程上下文中的邏輯流。
內(nèi)核
調(diào)度。每一個線程都有它自己的線程上下文(thread context)
.
線程ID(Thread ID,TID)
.所有運行在該進程里的線程
同享該進程的全部虛擬地址空間。
每一個進程開始生命周期時都是單1線程,這個線程稱為主線程(main thread)
。
對等線程(peer thread)
。 read
或sleep
間隔計時器
中斷。對等線程
。對等線程
履行1段時間,將控制傳遞回主線程。在某些方面,線程
履行是不同等于進程的。
線程
的上下文切換的開消比進程
的小很多,快很多線程
不是依照嚴格的父子層次來組織。 線程池(pool)
。 線程池
概念的主要影響是對等線程
終止。Posix線程
(Pthreads
)是在C程序中處理線程的1個標準接口。
Unix
系統(tǒng)可用這是我們第1個線程化的代碼,仔細解析。
線程的代碼和本地數(shù)據(jù)被封裝在1個線程例程(thread routine)
中。
2
行代碼所示:每一個線程例程
都以1個通用指針作為輸入,并返回1個通用指針。如果想傳遞多個參數(shù)給線程例程
指針
。如果想要線程例程
返回多個參數(shù)。
指針
。tid
寄存對等線程的線程ID
。
主線程調(diào)用pthread_create
函數(shù)創(chuàng)建1個新的對等線程(第7
行)。
pthread_create
的調(diào)用返回時,主線程和新創(chuàng)建的對等線程同時運行。通過調(diào)用pthread_join
,主線程等待對等線程
的終止。
對等線程
輸出Hello,world
。
主線程
終止。線程通過調(diào)用pthread_create
函數(shù)來創(chuàng)建其他線程。
#include<phread.h>
typedef void *(func)(void *);
int phread_create(pthread_t *tid,pthread_attr_t *attr,fun *f,void *arg)
//若成功則返回0,出錯則為非0
pthread_create
函數(shù)創(chuàng)建1個新的線程。
arg
,在新線程的上下文中運行線程例程f
.能用attr
參數(shù)改變新創(chuàng)建線程的默許屬性。
NULL
作為attr
的參數(shù)。pthread_create
返回時,參數(shù)tid
包括新創(chuàng)建線程的ID
。
pthread_self
函數(shù)來取得它自己的線程ID
。1個線程是以以下方式之1來終止
的
線程例程
返回時,線程會隱式地終止
。通過調(diào)用pthread_exit
函數(shù),線程會顯示地終止
。
pthread_exit
. thread_return
原型以下
#include<pthread.h>
void pthread_exit(void *thread_return)
//成功返回0,出錯返回非0
某個對等線程調(diào)用Unix
的exit
函數(shù),函數(shù)終止進程和所有與該進程有關(guān)的線程
。
對等線程
通過以當前線程ID為參數(shù)調(diào)用pthread_cancle
函數(shù)來終止當前線程。
原型
#include<pthread.h>
void pthread_cancle(pthread_t tid);
//成功返回0,出錯返回非0
線程通過調(diào)用pthread_join
函數(shù)等待其他進程終止
#include<pthread.h>
int pthread_join(pthread_t tid,void **thread_return);
//返回,成功則為0,出錯為非0
pthread_join
函數(shù)會阻塞,知道線程tid
終止,將線程返回的(void *
)指針賦值給thread_return
所指向的位置,然后回收已終止線程占用的存儲器資源。
pthread_join
不像wait
函數(shù)1樣等待任意1個線程的結(jié)束。
Stevens
在書中指出這是1個設(shè)計毛病。在任何1個時間點上,線程是可結(jié)合的(joinable)
或 是分離的(detached)
。
1個可結(jié)合的線程
能夠被其他線程收回其資源或殺死。
1個分離的線程
是不能被其他線程收回其資源或殺死。
pthread_detach
函數(shù)分離可結(jié)合線程tid
。
#include<pthread.h>
int pthread_detach(pthread_t tid);
返回:若成功則返回0,若出錯則返回非零。
pthread_once
函數(shù)允許你初始化與線程例程相干的狀態(tài)。
#include<pthread.h>
pthread_once_t once_control = PTHREAD_INIT;
int phread_once(phread_once_t *once_control,void (*init_routine)(void));
once_control
變量是1個全局或靜態(tài)變量,總是被初始化為PTHREAD_ONCE_INIT
.當你第1次用參數(shù)once_control
調(diào)用pthread_once
時,它調(diào)用init_routine
。
第2次,第3次以參數(shù)once_control
調(diào)用pthread_once
時,啥事也不產(chǎn)生。
當你需要動態(tài)初始化多個線程同享的全局變量時,pthread_once
函數(shù)是很有用的。
注意使用malloc
動態(tài)給1個connfdp
,否則可能兩個線程援用同1個connfdp
的地址。
競爭
為在線程例程
中避免存儲器泄漏,使用分離線程
。
主線程
中malloc
的變量。為了解1個C程序中的1個變量是不是同享,有1些基本的問題要解答
基礎(chǔ)存儲器模型
是甚么?變量實例
是如何映照到存儲器的?為了使同享討論具體化,使用下圖的程序作為示例。
示例程序由1個創(chuàng)建兩個對等線程
的主線程組成。主線程傳遞1個唯1的ID
給每一個對等線程,每一個對等線程利用這個ID
輸出1個個性化的信息,和調(diào)用該線程例程
的總次數(shù)。
線程化的C程序中的變量根據(jù)它們的存儲類型被映照到虛擬存儲器:
全局變量
全局變量
是定義在函數(shù)以外的變量。 讀/寫區(qū)域
包括每一個全局變量的1個實例。線程
都可以援用。5
行聲明的ptr
。本地自動變量
本地自動變量
就是定義在函數(shù)內(nèi)部但是沒有static
屬性的變量。 棧
包括它自己的所有本地自動變量的實例。本地靜態(tài)變量
本地靜態(tài)變量
是定義在函數(shù)內(nèi)部有static
屬性的變量。 讀/寫區(qū)域
。25
行的cnt
.我們說1個變量v
是同享
的,當期僅當它的1個實例被1個以上的線程
援用。
例如:
cnt
是同享的myid
不是同享的msgs
這類本地自動變量也能被同享是很重要的。同享變量10分方便,但是他們也引入了同步毛病(synchronization error)
的可能性。
斟酌下圖的程序。
到底哪里出錯了呢?這個毛病10分隱晦
,必須通過研究計數(shù)器循環(huán)
時的匯編代碼才能看出。
當badcnt.c
中的兩個對等線程在1個單處理器上并發(fā)履行
,機器指令以某種順序1個接1個地完成。同1個程序每次運行的順序都可能不同,這些順序
中有1些將會產(chǎn)生正確結(jié)果,但是其他的不會。這就是同步毛病
關(guān)鍵點
: 1般而言,你沒有辦法預(yù)測操作系統(tǒng)是不是將為你的線程選擇1個正確的順序。
cnt
正確的順序和毛病的順序(正確結(jié)果cnt=2
,毛病結(jié)果cnt=1
)我們可以借助于1種叫做進度圖(progress graph)
的方法來闡明這些正確和不正確的指令順序的概念。將在接下來介紹。
進度圖(process graph)
將n
個并發(fā)進程的履行模型化為1條n
維笛卡爾空間的軌跡線
。
每條軸k
對應(yīng)于k
的進度。
每一個點(I1,I2,I3,I4...,In)
代表線程k(k=1,...,n)
已完成到了Ik
這條指令的狀態(tài)。
圖的原點對應(yīng)于沒有任何線程完成這1條指令的初始狀態(tài)
。
進度圖
將指令履行模型化為從1個狀態(tài)到另外一個狀態(tài)的轉(zhuǎn)換(transition)
。
轉(zhuǎn)換
指從1點到相鄰1點的有向邊。 合法的轉(zhuǎn)換
是向各個軸的正半軸走。對線程i
,操作同享變量cnt
內(nèi)容的指令(Li,Ui,Si)
構(gòu)成了1個(關(guān)于同享變量cnt
的)臨界區(qū)(critical section)
。(必須確保指令要這樣履行)
這個臨界區(qū)
不應(yīng)當和其他線程的臨界區(qū)
交替履行。(這1段的指令不能交叉)。
我們要確保每一個線程在履行它的臨界區(qū)中的指令時,具有對同享變量的互斥的訪問(mutually exclusive access)
。
互斥(mutual exclusion)
。在進程圖中,兩個臨界區(qū)的交集情勢稱為不安全區(qū)(unsafe region)
。
安全軌跡線
。 不安全軌跡線
。我們必須以某種方式同步線程
,使它們總是有1條安全軌跡線
Edsger Dijksta
,并發(fā)編程領(lǐng)域的先鋒任務(wù),提出了1種經(jīng)典的解決同步不同履行線程問題
的方法
這類方法是基于1種叫做信號量(semaphore)
的特殊類型變量。
信號量s
是具有非負整數(shù)值的全局變量。
只能由兩種特殊的操作來處理,這兩種操作稱為P
和V
P(s),Proberen,測試
s
是非零的,那末P操作
將s
減1,并且立即返回。s
為零,那末就掛起這個線程,直到s
變成非零。 V
操作會重啟這個線程。P操作
將s
減1,并將控制返回給調(diào)用者。V(s),Verhogen,增加
V操作
將s
加1.P操作
等待s
變成非零。 V操作
隨機會重啟這些線程中的1個。s
減去1,完成它的P操作
。重點,P操作
和V操作
都是不可分割的,也就是本身確保了是1個帶有安全軌跡的操作。(所以又叫原語
)
cnt++
的操作。加1
這個操作中,加載,加1,存儲信號量進程是不可分割的。P
和V
的定義確保了1個正在運行的程序絕不可能進入這樣1種狀態(tài),也就是不可能有負值。
這個屬性叫做信號量不變性(semaphore invariant)
,為控制并發(fā)程序的軌跡線提供了強有力的工具。
信號量
提供了1種很方便的方法來確保對同享變量的互斥訪問。
基本的思想是
同享變量
(或1組相干的同享變量) 與1個信號量s(初始為
)`聯(lián)系起來。P(s)
和V(s)
操作相應(yīng)的臨界區(qū)包圍起來。以這類方式保護同享變量的信號量叫做2元信號量(binary semaphore)
以提供互斥為目的的2元信號量
常常也稱為互斥鎖(mutex)
。
P操作
叫做互斥鎖加鎖
。V操作
叫做互斥鎖解鎖
。占用這個互斥鎖
。1個被用作1組可用資源的計數(shù)器的信號量稱為計數(shù)信號量
。
關(guān)鍵思想:
P操作
和V操作
的結(jié)合創(chuàng)建了1組狀態(tài),叫做制止區(qū)(forbidden regin)
,其中s<0
信號量的不變形
,不可能有軌跡線進入這個區(qū)域制止區(qū)
包括了不安全區(qū)
的任何部份。 正確切現(xiàn)上文中的cnt
的線程同步。
第1步:聲明1個信號量 mutex
volatile int cnt = 0 ;
sem_t mutex;
第2步:主線程中初始化
Sem_init(&mutex,0,1);
第3步,在線程例程中對同享變量cnt
的更新包圍P
和V
操作,從而保護了它們。
for( i = 0 ;i < niters ;i++) {
P(&mutex);
cnt++;
V(&mutex);
}
除提供互斥
外,信號量的另外一個重要作用是調(diào)度對同享資源的訪問。
兩個經(jīng)典而有用的例子。
圖給出了生產(chǎn)者消費者問題
生產(chǎn)者線程
反復(fù)地生成新的項目
,并把它們插入到緩沖區(qū)中。消費者線程
不斷地從緩沖區(qū)取出這些項目
,然后消費使用它們。由于插入和取出項目都觸及更新同享變量
互斥的
緩沖區(qū)
的訪問。 我們將開發(fā)1個簡單的包,叫做SBUF
,用來構(gòu)造生產(chǎn)者-消費者程序。
SBUF
操作類型為sbuf_t
的有限緩沖區(qū)。
項目
寄存在1個動態(tài)分配的n
項整數(shù)數(shù)組(buf
)中。front
和rear
索引值記錄該隊列的第1項和最后1項。mutex
信號量提供互斥的緩沖區(qū)訪問slots
和items
信號量分別記錄空槽位和可用項目的數(shù)量。以下給出SBUF
函數(shù)的實現(xiàn):
sbuf_init
函數(shù)進行初始。 front
和rear
表示1個空的緩沖區(qū)。sbuf_deinit
函數(shù)是當利用程序使用完緩沖區(qū)時,釋放緩沖區(qū)存儲。sbuf_insert
sbuf_remove
讀者-寫著
問題是互斥問題的1個概括。
1組并發(fā)的線程要訪問同1個數(shù)據(jù)對象。
寫者
讀者
寫者
必須具有對對象的獨占訪問。
讀者
可以和無窮多個其他讀者同享對象。讀者-寫者
問題有幾個變種,都是基于讀者和寫者的優(yōu)先級
第1類讀者-寫者問題
讀者
優(yōu)先,要求不要讓讀者等待,除非已把1個使用權(quán)限賦予了1個寫者
。讀者
不會由于有1個寫者
在等待而等待。第2類讀者-寫者問題(?)
寫者
優(yōu)先,要求1但1個寫者準備好可以寫,它就會盡量地完成它的寫操作。給出第1類讀者-寫者問題答案。
信號量w
控制對訪問同享對象的臨街區(qū)的訪問。
讀者
w
只對第1個讀者上鎖w
對最后1個走的讀者解鎖寫者
w
上鎖w
解鎖mutex
保護對同享變量readcnt
的訪問。 readcnt
統(tǒng)計當前臨界區(qū)的讀者數(shù)量。所有讀者-寫者
答案都有可能致使饑餓
為每一個新的客戶端創(chuàng)建新的線程,有很多的代價。
1個基于預(yù)線程化
的服務(wù)器利用生產(chǎn)者-消費者模型構(gòu)造1個更高效力的方式。
主要用于多核CPU的算法。
比如:利用并行來完成n路遞歸
互斥
和生產(chǎn)者-消費者同步
的技術(shù),只是并提問題的冰山1角。
同步問題
從根本來講是很難的問題。
這章我們以線程
為例討論。
同步問題
在任何并發(fā)流
操作同享資源時都會出現(xiàn)。 信號
時,回收進程時的競爭
。1個函數(shù)被稱為線程安全的(thread-safe)
,當且僅當被多個并發(fā)線程反復(fù)地調(diào)用時,它會1直產(chǎn)生正確的結(jié)果。否則就是線程不安全的(thread-unsafe)
我們能夠定義出4個
(不相交)線程不安全函數(shù)類:
P,V
這樣的同步操作來保護同享的變量第 2 類 : 保持逾越多個調(diào)用狀態(tài)的函數(shù)
1個偽隨機數(shù)生成器
是這類線程不安全的例子。
由于產(chǎn)生的結(jié)果依賴于上1個next
的值。
seed
不管運行多少次,都是一樣的結(jié)果。線程不安全
的解決方案: 重寫
static
,而是依托調(diào)用者在參數(shù)中傳遞狀態(tài)信息。第 3 類 :返回指向靜態(tài)變量的指針的函數(shù)( 有點類似第1類 )
解放方案
指針
。加鎖-拷貝技術(shù):
用上面的原理寫1個線程不安全函數(shù)
的包裝函數(shù)來實現(xiàn)線程安全。
第 4 類 : 調(diào)用線程不安全函數(shù)的函數(shù)。
f
調(diào)用線程不安全函數(shù)g
。那末f
可能不安全。 g
是第2類,那末f
1定不安全,也沒有辦法去修正,只能改變g
.g
是第1,3類,可以用加鎖-拷貝技術(shù)來解決。有1類重要的線程安全函數(shù)
,叫做可重入函數(shù)(reentrant function)
其特點在于它們有這樣1種屬性。
同享數(shù)據(jù)
。被分為兩類
隱式可重入
參數(shù)可以有指針
是不是可重入,同時取決于調(diào)用者
,和被調(diào)用者
。
可重入函數(shù)
比較高效是由于不需要同步操作。
認識到可重入性有時即是調(diào)用者
也是被調(diào)用者
的屬性。
被調(diào)用者
的單獨屬性。大多數(shù)Unix
函數(shù),包括大部份定義在標準C庫的函數(shù)(malloc
,free
,realloc
,printf
和scanf
)都是線程安全的。
asctime
,ctime
,localtime
函數(shù)是在不同時間和數(shù)據(jù)格式相互來回轉(zhuǎn)換時常常使用的函數(shù)。
gethostbyname
,gethostbyaddr
,inet_ntoa
函數(shù)是常常用的網(wǎng)絡(luò)編程函數(shù)。
strtok
函數(shù)是1個過時了的同來分析字符串的函數(shù)。
Unix
系統(tǒng)提供大多數(shù)線程不安全函數(shù)的可重入版本。
_r
后綴結(jié)尾。gethostbyname_r
。當1個程序的正確性依賴于1個線程要在另外一個線程到達y
點之前到達它的控制流中的x
點,就會產(chǎn)生競爭
。
競爭
產(chǎn)生的理由是由于程序員假定某種特殊的軌跡線穿過履行狀態(tài)空間。例子:
程序10分簡單。
主線程創(chuàng)建了4個對等線程
,并傳遞1個指向循環(huán)變量i
的指針作為線程的ID
。并輸出。
i
1定是4個不同的。所以會想固然覺得會輸出4個不同的ID
。對等線程
給myid
賦值結(jié)束后,i
才會自增。i++
,和對等線程myid=*((int *)vargp)
的 競爭
。解決方案:用1個臨時地址保存i
信號量
引入了1種潛伏的使人討厭的運行時毛病,叫做死鎖 (deadlock
)。
進度圖
對理解死鎖是1個無價的工具。
死鎖的區(qū)域d
是1個只能進,不能出的區(qū)域。
制止區(qū)
,能進去。制止區(qū)
了。如果制止區(qū)不堆疊,1定不會產(chǎn)生死鎖
。
死鎖
。死鎖是1個相當困難的問題,由于它總是不可預(yù)測的。
死鎖
區(qū)域。使用2元信號量來實現(xiàn)互斥,可以利用1下有效的規(guī)則。
互斥鎖加鎖順序規(guī)則
:如果對程序中每對互斥鎖(s,t)
,每一個占用s
和t
的線程都依照相同的順序?qū)λ鼈兗渔i,那末這個程序就是無死鎖的。
GGGGGGGGGGG,暫時告1段落了!!!!!!!!!!!!!!ddd!!