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