多線程編程--使用同步對象編程

首先定義了四種可用的同步類型,並且討論實現同步的注意事項。
互斥鎖(mutex)
條件變量(condition variable)
多讀單寫鎖(multi-read,single-write lock)
信號量(semophore)
進程間同步(process synchronization)
同步原語的比較(compare primitive)

同步對象是內存中的變量,你可以象訪問一般的數據那樣來訪問它。不同進程內的線程可以通過共享內存中的同步變量來同步,即使這些線程互不可見。
同步變量可以放置在文件當中,可以比創建它的進程擁有更長的生命。
同步對象的類型包括:
· 互斥鎖
· 狀態變量
· 讀寫鎖
· 信號燈(信號量)
在下面幾種情況下,同步是重要的:
· 在兩個或更多個進程內的線程可以合用一個同步變量。注意,同步變量應當被一個進程初始化,在第二次初始化時,該同步變量被設置爲解鎖狀態。
· 同步是唯一保證共享數據持久的辦法。
· 一個進程可以映射一個文件並通過一個線程將其加鎖,修改完成之後,該線程釋放文件鎖並恢復文件。在文件加鎖的過程中,任何程序中的任何 線程想要加鎖時都會阻塞,直至解鎖;
· 同步可以保證易變數據的安全。
· 同步對於簡單變量也是很重要的,例如整數。在整數沒有和總線對齊或
大於數據寬度的情況下,讀寫一個整數可能需要多個內存週期。雖然在SPARC系統上不會發生這樣的情況,但移植程序時不能不考慮這一點;

3.1互斥鎖

用互斥鎖可以使線程順序執行。互斥鎖通常只允許一個線程執行一個關鍵部分的代碼,來同步線程。互斥鎖也可以用來保護單線程代碼。
Table 3-1 互斥鎖函數
函數 操作
Mutex_init(3T) 初始化一個互斥鎖
Mutext_lock(3T) 給一個互斥鎖加鎖
Mutex_trylock(3T) 加鎖,如失敗不阻塞
Mutex_unlock(3T) 解鎖
Mutex_destroy(3T) 解除互斥狀態
如果兩個進程有共享且可寫的內存,且做了相應的初始化設置後(參見mmap(2)),互斥鎖可以實現進程間的線程同步。
互斥鎖在使用前一定要初始化。
多線程等待一個互斥鎖時,其獲得互斥鎖的順序是不確定的。 3.1.1初始化一個互斥鎖

mutex_init(3T)
#include ( or #include )
int mutex_init(mutex_t *mp, int type, void * arg);
用mutex_init()來初始化一個由mp指向的互斥鎖。Type可以是以下值之一(arg現在先不談)。
USYNC_PROCESS 互斥鎖用來同步進程間的線程。
USYNC_THREAD 互斥鎖只用來同步進程內部的線程。
互斥鎖也可以通過分配零內存來初始化,在此種情況下應當設定USYNC_THREAD。
一定不會有多個線程同時初始化同一個互斥鎖。一個互斥鎖在使用期間一定不會被重新初始化。
返回值--mutex_init()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數
EFAULT mp或者arg指向一個非法地址。

3.1.2給互斥鎖加鎖

mutex_lock(3T)
#include (or #include )
int mutex_lock(mutex_t *mp);
用mutex_lock()鎖住mp指向的互斥鎖。如果mutex已經被鎖,當前調用線程阻塞直到互斥鎖被其他線程釋放(阻塞線程按照線程優先級等待)。當mutex_lock()返回,說明互斥鎖已經被當前線程成功加鎖。
返回值--mutex_lock()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數
EFAULT mp指向一個非法地址。

3.1.3加非阻塞互斥鎖

mutex_trylock(3T)
#include (or #include )
int mutex_trylock(mutex_t *mp);
用mutex_trylock()來嘗試給mp指向的互斥鎖加鎖。這個函數是mutex_lock()的非阻塞版本。當一個互斥鎖已經被鎖,本調用返回錯誤。否則,互斥鎖被調用者加鎖。
返回值--mutex_trylock()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數
EFAULT mp指向一個非法地址。
EBUSY mp指向的互斥鎖已經被鎖。

3.1.4給互斥鎖解鎖

mutex_unlock(3T)
#include (or #include )
int mutex_unlock(mutex_t *mp);
用mutex_unlock()給由mp指向的互斥鎖解鎖。互斥鎖必須處於加鎖狀態且調用本函數的線程必須是給互斥鎖加鎖的線程。如果有其他線程在等待互斥鎖,在等待隊列頭上的線程獲得互斥鎖並脫離阻塞狀態。
返回值--mutex_unlock()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數
EFAULT mp指向一個非法地址。

3.1.5清除互斥鎖

mutex_destroy(3T)
#include (or #include )
int mutex_destroy(mutex_t *mp);
用mutex_destroy()函數解除由mp指向的互斥鎖的任何狀態。儲存互斥鎖的內存不被釋放。
返回值--mutex_destroy()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數
EFAULT mp指向一個非法地址。

3.1.6互斥鎖代碼示例
Code Example 3-1 Mutex Lock Example
Mutex_t count_mutex;
Int count;

Increment_count()
{ mutex_lock(&count_mutex);
count=count+1;
mutex_unlock(&cout_mutex);
}
int get_count()
{ int c;
mutex_lock(&count_mutex);
c=count;
mutex_unlock(&count_mutex);
return(c);
}
在示例3-1中兩個函數用互斥鎖實現不同的功能,increment_count()保證對共享變量的一個原子操作(即該操作不可中斷),get_count()用互斥鎖保證讀取count期間其值不變。

*爲鎖設置等級

你可能會需要同時訪問兩種資源。也許你在用其中一種資源時,發現需要另外一 種。就象我們在示例3-2中看到的,如果兩個線程希望佔有兩種資源,但加互斥鎖的 順序不同,有可能會發生問題。在這個例子當中,兩個線程分別給互斥鎖1和2加鎖, 在它們想給另外的資源加鎖的時候,將會發生死鎖。
Code Example 3-2 Deadlock
Thread 1:

Mutex_lock(&m1)
/* use resource 1*/
mutex_lock(&m2);
/* use resources 1 and 2*/
mutex_unlock(&m2);
mutex_unlock(&m1);

Thread 2:

Mutex_lock(&m2);
/*use resource 2*/
mutex_lock(&m1);
/* use resources 1 and 2*/
mutex_unlock(&m1);
mutex_unlock(&m2);
避免這個問題的最好辦法是在線程給多個互斥鎖加鎖時,遵循相同的順序。這種技術的一種實現叫"鎖的等級":在邏輯上爲每個鎖分配一個數進行排序。
如果你已經擁有一個等級爲I的互斥鎖,你將不能給等級小於I的互斥鎖加鎖。
---------------------------------------
注意--lock_init可以檢測這個例子當中死鎖的類型。避免死鎖的最好辦法是採用等
級鎖:如果對互斥鎖的操作遵循一個預先定義的順序,死鎖將不會發生。
---------------------------------------
但是,這種技術並非總可以使用--有時你必須對互斥鎖進行不按照預定義順序的 操作。爲了在這種情況下阻止死鎖,一個線程在發現死鎖用其他方法無法避免時, 必須釋放已經佔有的所有資源。示例3-3顯示了這種方法。

Code Example 3-3 條件鎖
Thread 1:
Mutex_lock(&m1);
Mutex_lock(&m2);
Mutex_unlock(&m2);
Mutex_unlock(&m1);

Thread 2:
For(;:wink:{
Mutex_lock(&m2);
If(mutex_trylock(&m1)==0)
/*got it*/
break;
/*didn't get it */
mutex_unlock(&m1);
}
mutex_unlock(&m1);
mutex_unlock(&m2);
在上例中,線程1按照預定的順序加鎖,但線程2打亂了次序。爲避免死鎖,線程2必須小心操作互斥鎖1:如果設置在等待互斥鎖釋放時阻塞,則可能導致死鎖。
爲保證上述情況不會發生,線程2調用mutex_trylock,如果互斥鎖可用則用, 不可用則立刻返回失敗。在這個例子當中,線程2一定要釋放互斥鎖2,以便線程1 可以使用互斥鎖1和互斥鎖2。

3.1.7鎖內嵌於單鏈表當中

示例3-4同時佔有3個鎖,通過鎖等級定義避免死鎖。
Code Example 3-4 單鏈表結構
Typedef struct node1{
Int value;
Struct node1 *link;
Mutex_t lock;
}node1_t;
node1_t Listhead;
此例利用單鏈表結構的每一個節點存儲一個互斥鎖。爲了刪除一個互斥鎖,要從listhead開始搜索(它本身不會被刪除),知道找到指定的節點。
爲了保證同時刪除不會發生,在訪問其內容之前要先鎖定節點。因爲所有的搜索從listhead開始按順序進行,所以不會出現死鎖。
如果找到指定節點,對該節點和其前序節點加鎖,因爲兩個節點都需要改變。因爲前序節點總是首先加鎖,死鎖將不會發生。
下面C程序從單鏈表中刪除一項。
Code Example 3-5 內嵌鎖的單鏈表
Node1_t * delete(int value){
Node1_t * prev, *current;
Prev =&listhead;
Mutex_lock(&prev->lock);
While((current=prev->link)!=NULL){
Mutex_lock(¤t->lock);
If(current->value==value){
Prev->link=current->link;
Mutex_unlock(¤t->lock);
Mutex_unlock(&prev->lock);
Current->link=NULL;
Return(current);
}
mutex_unlock(&prev->lock);
prev=current;
}
mutex_unlock(&prev->lock);
return(NULL);
}

3.1.8內嵌在環狀鏈表中的鎖

示例3-6把前例的單鏈表改爲環鏈表。環鏈表沒有顯式的表頭;一個線程可以和某個節點連接,對該節點及其鄰節點進行操作。等級鎖在這裏不容易使用,因爲其鏈表是環狀的。
Code Example 3-6 Circular Linked List Structure
Typedef struct node 2 {
Int value;
Struct node2 *link;
Mutex_t lock;
} node2_t;

下面的C程序給兩個節點加鎖,並對它們做操作。
Code Example 3-7 內嵌鎖的環鏈表
Void Hit Neighbor(node2_t *me){
While(1){
Mutex_lock(&me->lock);
If(mutex_lock(&me->link->lock)){
/* failed to get lock*/
mutex_unlock(&me->lock);
continue;
}
break;
}
me->link->value += me->value;
me->value /=2;
mutex_unlock(&me->link->lock);
mutex_unlock(&me->lock);
}

3.2條件變量

用條件變量來自動阻塞一個線程,直到某特殊情況發生。通常條件變量和互斥鎖同時使用。
Table3-2 有關條件變量的函數
函數 操作
Cond_init(3T) 初始化條件變量
Cond_wait(3T) 基於條件變量阻塞
Cond_signal(3T) 解除指定線程的阻塞
Cond_timedwait(3T) 阻塞直到指定事件發生
Cond_broadcast(3T) 解除所有線程的阻塞
Cond_destroy(3T) 破壞條件變量
通過條件變量,一個線程可以自動阻塞,直到一個特定條件發生。條件的檢測是在互斥鎖的保護下進行的。
如果一個條件爲假,一個線程自動阻塞,並釋放等待狀態改變的互斥鎖。如 果另一個線程改變了條件,它發信號給關聯的條件變量,喚醒一個或多個等待它 的線程,重新獲得互斥鎖,重新評價條件。
如果兩進程共享可讀寫的內存,條件變量可以被用來實現這兩進程間的線程同步。
使用條件變量之前要先進行初始化。而且,在有多個線程等待條件變量時,它們解除阻塞不存在確定的順序。

3.2.1初始化條件變量

cond_init(3T)
#include (or #include )
int cond_init(cond_t *cvp, int type, int arg);
用cond_init()初始化有cvp指向的條件變量。Type可以是如下值之一(arg先
不談):
USYNC_PROCESS 條件變量可以在進程間實現線程同步;
USYNC_THREAD 條件變量只能在進程內部對線程同步;
條件變量可以用分配零內存來初始化,在這種情況下一定要是USYNC_THREAD。
多線程不能同時初始化同一個條件變量。如果一個條件變量正在使用,它不能被重新初始化。
返回值--cond_init()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數
EFAULT mp指向一個非法地址。

3.2.2關於條件變量阻塞

cond_wait(3T)
#include (or #include )
int cond_wait(cond_t *cvp, mutex_t *mp);
用cond_wait()釋放由mp 指向的互斥鎖,並且使調用線程關於cvp指向的條件 變量阻塞。被阻塞的線程可以被cond_signal(), cond_broadcast(),或者由fork() 和傳遞信號引起的中斷喚醒。
與條件變量關聯的條件值的改變不能從cond_wait()的返回值得出,這樣的狀 態必須被重新估價。
即使是返回錯誤信息,Cond_wait()通常在互斥鎖被調用線程加鎖後返回。
函數阻塞直到條件被信號喚醒。它在阻塞前自動釋放互斥鎖,在返回前在自動 獲得它。
在一個典型的應用當中,一個條件表達式在互斥鎖的保護下求值。如果條件表 達式爲假,線程基於條件變量阻塞。當一個線程改變條件變量的值時,條件變量獲 得一個信號。這使得等待該條件變量的一個或多個線程退出阻塞狀態,並試圖得到 互斥鎖。
因爲在被喚醒的線程的cond_wait()函數返回之前條件已經改變,導致等待的 條件在得到互斥鎖之前必須重新測試。推薦的辦法是在while循環中寫條件檢查。

Mutex_lock();
While(condition_is_false)
Cond_wait();
Mutes_unlock();
如果有多個線程關於條件變量阻塞,其退出阻塞狀態的順序不確定。
返回值--cond_wait()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EFAULT cvp指向一個非法地址。
EINTR 等待被信號或fork()中斷。

3.2.3使指定線程退出阻塞狀態

cond_signal(3T)
#include (or #include )
int cond_signal (cond_t *cvp);
用cond_signal()使得關於由cvp指向的條件變量阻塞的線程退出阻塞狀態。在 同一個互斥鎖的保護下使用cond_signal()。否則,條件變量可以在對關聯條件變量 的測試和cond_wait()帶來的阻塞之間獲得信號,這將導致無限期的等待。
如果沒有一個線程關於條件變量阻塞,cond_signal無效。
返回值--cond_signal()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EFAULT cvp指向一個非法地址。
Code Example 3-8 使用cond_wait(3T)和cond_signal(3T)的例子
Mutex_t count_lock;
Cond_t count_nonzero;
Unsigned int count;
Decrement_count()
{
mutex_lock(&count_lock);
while(count==0)
cond_wait(&count_nonzero,&count_lock);
count=count-1;
mutex_unlock(&count_lock);
}
increment_count()
{
mutex_lock(&count_lock);
if(count==0)
cond_signal(&count_nonzero);
count=count+1;
mutex_unlock(&count_lock);
}

3.2.4阻塞直到指定事件發生

cond_timedwait(3T)
#include (or #include )
int cond_timedwait(cond_t *cvp, mutex_t *mp,
timestruc_t *abstime);
cond_timedwait()和cond_wait()用法相似,差別在於cond_timedwait()在經過有abstime指定的時間時不阻塞。
即使是返回錯誤,cond_timedwait()也只在給互斥鎖加鎖後返回。
Cond_timedwait()函數阻塞,直到條件變量獲得信號或者經過由abstime指定 的時間。Time-out被指定爲一天中的某個時間,這樣條件可以在不重新計算 time-out值的情況下被有效地重新測試,???就象在示例3-9中那樣。
返回值--cond_timedwait()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 由abstime 指定的時間大於應用程序啓動的時間加50,000,000,或者納秒數大於等於1,000,000,000。
EFAULT cvp指向一個非法地址。
EINTR 等待被信號或fork()中斷。
ETIME abstime指定的時間已過。
Code Example 3-9 時間條件等待
Timestruc_t to;
Mutex_t m;
Cond_t c;
Mutex_lock(&m);
To.tv_sec=time(NULL)+TIMEOUT;
To.tv_nsec=0;
While (cond==FALSE){
Err=cond_timedwait(&c,&m,&to);
If(err=ETIME) {
/* TIMEOUT, do something */
break;
}
}
mutex_unlock(&m);

3.2.5使所有線程退出阻塞狀態

cond_broadcast(3T)
#include ( or #include )
int cond_wait(cond_t *cvp);
用cond_broadcast()使得所有關於由cvp指向的條件變量阻塞的線程退出阻塞狀態。如果沒有阻塞的線程,cond_broadcast()無效。
這個函數喚醒所有由cond_wait()阻塞的線程。因爲所有關於條件變量阻塞的線程都同時參與競爭,所以使用這個函數需要小心。
例如,用cond_broadcast()使得線程競爭變量資源,如示例3-10所示。
Code Example 3-10 條件變量廣播
Mutex_t rsrc_lock;
Cond_t rsrc_add;
Unsigned int resources;

Get_resources(int amount)
{ mutex_lock(&rsrc_lock);
while(resources < amount) {
cond_wait(&rsrc_add, &rsrc_lock);
}
resources-=amount;
mutex_unlock(&rsrc_lock);
}
add_resources(int amount)
{
mutex_lock(&rsrc_lock);
resources +=amount;
cond_broadcast(&rsrc_add);
mutex_unlock(&rsrc_lock);
}
注意,在互斥鎖的保護內部,首先調用cond_broadcast()或者首先給resource增值,效果是一樣的。
返回值--cond_broadcast()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EFAULT cvp指向一個非法地址。
在互斥鎖的保護下調用cond_broadcast()。否則,條件變量可能在檢驗關聯狀態和通過cond_wait()之間獲得信號,這將導致永久等待。

3.2.6清除條件變量

cond_destroy(3T)
#include ( or #include )
int cond_destroy(cond_t *cvp);
使用cond_destroy() 破壞由cvp指向的條件變量的任何狀態。但是儲存條件變量的空間將不被釋放。
返回值--cond_destroy()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EFAULT cvp指向一個非法地址。

3.2.7喚醒丟失問題

在沒有互斥鎖保護的情況下調用cond_signal()或者cond_broadcast()會導致丟 失喚醒問題。一個喚醒丟失發生在信號或廣播已經發出,但是線程即使在條件爲真 時仍然關於條件變量阻塞,具體地說,這發生在調用cond_signal()時並沒有獲得互 斥鎖的情況下。
如果一個線程已經作過條件檢驗,但是尚未調用cond_wait(),這時另外一個線 程調用cond_signal(),因爲沒有已被阻塞的線程,喚醒信號丟失。
3.2.8生產者/消費者問題

這個問題是一個標準的、著名的同時性編程問題的集合:一個有限緩衝區和兩類線程,生產者和消費者,他們分別把產品放入緩衝區和從緩衝區中拿走產品。
一個生產者在緩衝區滿時必須等待,消費者在緩衝區空時必須等待。
一個條件變量代表了一個等待條件的線程隊列。
示例3-11有兩個隊列,一個(less)給生產者,它們等待空的位置以便放入信 息;另外一個(more)給消費者,它們等待信息放入緩衝區。這個例子也有一個互 斥鎖,它是一個結構,保證同時只有一個線程可以訪問緩衝區。
下面是緩衝區數據結構的代碼。
Code Example 3-11 生產者/消費者問題和條件變量
Typedef struct{
Char buf[BSIZE];
Int occupled;
Int nextin;
Int nextout;
Mutex_t mutex;
Cond_t more;
Cond_t less;
}buffer_t;
buffer_t buffer;
如示例3-12所示,生產者用一個互斥鎖保護緩衝區數據結構然後確定有足夠的空 間來存放信息。如果沒有,它調用cond_wait(),加入關於條件變量less阻塞的線程 隊列,說明緩衝區已滿。這個隊列需要被信號喚醒。
同時,作爲cond_wait()的一部分,線程釋放互斥鎖。等待的生產者線程依賴於 消費者線程來喚醒。當條件變量獲得信號,等待less的線程隊列裏的第一個線程被喚 醒。但是,在線程從cond_wait()返回前,必須獲得互斥鎖。
這再次保證了線程獲得對緩衝區的唯一訪問權。線程一定要檢測緩衝區有足夠的 空間,如果有的話,它把信息放入下一個可用的位置裏。
同時,消費者線程也許正在等待有信息放入緩衝區。這些線程等待條件變量more。 一個生產者線程,在剛剛把信息放入存儲區後,調用cond_signal()來喚醒下一個等 待的消費者。(如果沒有等待的消費者,這個調用無效。)最後,生產者線程釋放互 斥鎖,允許其他線程操作緩衝區。

Code Example 3-12 生產者/消費者問題--生產者
Void producer(buffer_t *b, char item) {
Mutex_lock(&b->mutex);

While ( b->occupied >= BSIZE)
Cond_wait(&b->less, &b->mutex);
Assert(b->occupied < BSIZE);
b->buf(b->nextin++)=item;
b->nextin %=BSIZE;
b->occupied ++;
/* now: either b->occupied < BSIZE and b->nextin is the index
of the next empty slot in the buffer, or
b->occupied == BSIZE and b->nextin is the index of the
next (occupied) slot that will be emptied by a consumer
(such as b-> == b->nextout) */

cond_signal(&b->more);
mutex_unlock(&b->mutex);
}
注意assert()命令的用法;除非代碼用NDEBUG方式編譯,assert()在參數爲真時 (非零值)不做任何操作,如果參數爲假(參數爲假),程序退出。
這種聲明在多線程編程中特別有用--在失敗時它們會立刻指出運行時的問題, 它們還有其他有用的特性。
後面說明代碼可以更加稱得上是聲明,但它太過複雜,無法用布爾表達式來表達,所以用文字來寫。???
聲明和說明???都是不變量的實例。它們都是一些邏輯命題,在程序正常執行時不應當被證僞,除非一個線程試圖改變非變量說明段的變量。???
不變量是一種極爲有用的技術。即使它們沒有在程序中寫出,在分析程序中也需要把它們看成不變量。
生產者代碼中的不變量(說明部分)在程序執行到這一段時一定爲真。如果你把這段說明移到mutex_unlock()後面,它將不一定保持爲真。如果將其移到緊跟着聲明的後面,它仍然爲真。
關鍵在於,不變量表現了一個始終爲真的屬性,除非一個生產者或一個消費者正 在改變緩衝區的狀態。如果一個線程正在操作緩衝區(在互斥鎖的保護下),它將暫 時將不變量置爲假。但是,一旦線程結束對緩衝區的操作,不變量會立刻恢復爲真。
示例3-13爲消費者的代碼。它的流程和生產者是對稱的。
Code Example 3-13 生產者/消費者問題--消費者
Char consumer(buffer_t *b){
Char item;
Mutex_lock(&b->mutex);
While(b->occupied <=0)
Cond_wait(&b->more, &b->mutex);
Assert(b->occupied>0);
Item=b->buf(b->nextout++);
b->nextout %=BSIZE;
b->occupied--;
/* now: either b->occupied>0 and b->nextout is the index of
the nexto ccupied slot in the buffer, or b->occupied==0
and b->nextout is the index of the next(empty) slot that
will be filled by a producer (such as b->nextout ==b->nextin) */
cond_signal(&b->less);
mutex_unlock(&b->mutex);
return(item);
}

3.3多讀單寫鎖

讀寫鎖允許多個線程同時進行讀操作,但一個時間至多隻有一個線程進行寫操作。
表3-3 讀寫鎖的函數
函數 操作
rwlock_init(3T) 初始化一個讀寫鎖
rw_rdlock(3T) 獲得一個讀鎖
rw_tryrdlock(3T) 試圖獲得一個讀鎖
rw_wrlock(3T) 獲得一個寫鎖
rw_trywrlock(3T) 試圖獲得一個寫鎖
rw_unlock(3T) 使一個讀寫鎖退出阻塞
rwlock_destroy(3T) 清除讀寫鎖狀態
如果任何線程擁有一個讀鎖,其他線程也可以擁有讀鎖,但必須等待寫鎖。如 果一個線程擁有寫鎖,或者正在等待獲得寫鎖,其它線程必須等待獲得讀鎖或寫鎖。
讀寫鎖比互斥鎖要慢,但是在所保護的數據被頻繁地讀但並不頻繁寫的時候可以提高效率。
如果兩個進程有共享的可讀寫的內存,可以在初始化時設置成用讀寫鎖進行進程間的線程同步。
讀寫鎖使用前一定要初始化。

3.3.1初始化一個讀寫鎖
rwlock_init(3T)
#include (or #include )
int rwlock_init(rwlock_t *rwlp, int type, void * arg);
用rwlock_init()來初始化由rwlp指向的讀寫鎖並且設置鎖的狀態爲沒有鎖。
Type可以是如下值之一(arg現在先不談)。
USYNC_PROCESS 讀寫鎖可以實現進程間的線程同步。
USYNC_THREAD 讀寫鎖只能在進程內部實現線程同步。
多線程不能同時初始化一個讀寫鎖。讀寫鎖可以通過分配零內存來初始化,在這種情況下,一定要設置USYNC_THREAD。一個讀寫鎖在使用當中不能被其他線程重新初始化。
返回值--rwlock_init()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT rwlp或arg指向一個非法地址。

3.3.2獲得一個讀鎖

rw_rdlock(3T)
#include (or #include )
int rw_rdlock(rwlock_t *rwlp);
用rw_rdlock()來給一個由rwlp指向的讀寫鎖加上讀鎖。如果讀寫鎖已經被加寫鎖,則調用線程阻塞直到寫鎖被釋放。否則,讀鎖將被成功獲得。
返回值--rw_rdlock()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT rwlp指向一個非法地址。

3.3.3試圖獲得一個讀鎖

rw_tryrdlock(3T)
#include (or #include )
int rw_tryrdlock(rwlock_t *rwlp);
試圖給讀寫鎖加讀鎖,如果讀寫鎖已經被加寫鎖,則返回錯誤,而不再進入阻塞狀態。否則,讀鎖將被成功獲得。
返回值--rw_tryrdlock ()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT rwlp指向一個非法地址。
EBUSY 由rwlp指向的讀寫鎖已經被加寫鎖。

3.3.4獲得一個寫鎖

rw_wrlock(3T)
#include (or #include )
int rw_wrlock(rwlock_t *rwlp);
用rw_wrlock()爲由rwlp指向的讀寫鎖加寫鎖。如果該讀寫鎖已經被加讀鎖或寫鎖,則調用線程阻塞,直到所有鎖被釋放。一個時刻只有一個線程可以獲得寫鎖。
返回值--rw_wrlock ()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT rwlp指向一個非法地址。

3.3.5試圖獲得寫鎖

rw_trywrlock(3T)
#include (or #include )
int rw_trywrlock(rwlock_t *rwlp);
用rw_trywrlock()試圖獲得寫鎖,如果該讀寫鎖已經被加讀鎖或寫鎖,它將返回錯誤。
返回值--rw_trywrlock ()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT rwlp指向一個非法地址。
EBUSY 由rwlp指向的讀寫鎖已被加鎖。

3.3.6使一個讀寫鎖退出阻塞狀態

rw_unlock(3T)
#include (or #include )
int rwlock_tryrdlock(rwlock_t *rwlp);
用rw_unlock()來使由rwlp指向的讀寫鎖退出阻塞狀態。調用線程必須已經獲得對該讀寫鎖的讀鎖或寫鎖。如果任何其它線程在等待讀寫鎖可用,它們當中的一個將退出阻塞狀態。
返回值--rw_unlock ()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT rwlp指向一個非法地址。

3.3.7清除讀寫鎖

rwlock_destroy(3T)
#include (or #include )
int rwlock_destroy(rwlock_t *rwlp);
使用rwlock_destroy()來取消由rwlp指向的讀寫鎖的狀態。存儲讀寫鎖的空間不被釋放。
返回值--rw_destroy ()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT rwlp指向一個非法地址。
示例3-14用一個銀行帳戶來演示讀寫鎖。如果一個程序允許多個線程同時進行讀操作,一個時刻只有一個寫操作被允許。注意get_balance()函數通過鎖來保證檢查和儲存操作是原子操作。
Code Example 3-14 讀/寫銀行帳戶
Rwlock_t account_lock;
Float checking_balance=100.0;
Float saving_balance=100.0;
… …
rwlock_init (&account_lock, 0, NULL);
… …
float get_balance(){
float bal;
rw_rdlock(&account_lock);
bal=checking_balance +saving_balance;
rw_unlock(&account_lock);
return(bal);
}
void tranfer_checking_to_savings(float amount) {
rw_wrlock(&account_lock);
checking_balance=checking_balance - amount;
savings_balance=savings_balance +amount;
rw_unlock(&account_lock);
}

3.4信號量(信號燈)

信號燈是E.W.Dijkstra在60年代晚期定義的程序結構。Dijkstra的模型是一個鐵路上的操作:一段單線鐵路在一個時刻只允許一列火車通過。
用一個信號燈來維護這段鐵路。一列火車在進入單線鐵路之前必須等待信號燈 的許可。如果一列火車進入這段軌道,信號燈改變狀態,以防止其他火車進入。在 火車離開這段軌道時,必須將信號燈復原,使得其他火車得以進入。
在信號燈的計算機版本中,一個信號燈一般是一個整數,稱之爲信號量。一個 線程在被允許進行後對信號量做一個p操作。
P操作的字面意思是線程必須等到信號量的值爲正(positive)才能繼續進行, 進行前先給信號量減1。當做完相關的操作時(相當於離開鐵軌),線程執行一個 v操作,即給信號量加1。這兩個操作必須具有不可中斷性,也叫不可分性,英文字 面爲原子性(atomic),即他們不能被分成兩個子操作,在子操作之間還可以插入 其它線程的其他操作,這些操作可能改變信號量。在P操作中,信號量的值在被減之 前一定要爲正(使得信號量在被減1之後不會爲負)。
在P操作或V操作當中,操作不會互相干擾。如果兩個V操作要同時執行,則信號量的新值比原來大2。
記住P和V本身是什麼意思已經不重要了,就象記住Dijkstra是荷蘭人一樣。但 是,如果引起了學者考證的興趣,P代表prolagen,一個由proberen de verlagen演 變來的合成詞,它的意思是"試圖減"。V代表verhogen,它的意思是"增加"。這些在 Dijkstra的技術筆記EWD 74中提到過。
Sema_wait(3T)和sema_post(3T)分別對應Dijkstra的P和V操作, sema_trywait(3T)是P操作的一個可選的形式,在P操作不能執行時,線程不會阻塞, 而是立刻返回一個非零值。
有兩種基本的信號量:二值信號量,其值只能是0或者1,和計數信號量,可以 是非負值。一個二值信號量在邏輯上相當於一個互斥鎖。
然而,儘管並不強制,互斥鎖應當被認爲只能被擁有鎖的線程釋放,而"擁有信 號量的線程"這個概念是不存在的,任何線程都可以進行一個V操作 (或sema_post(3T))。
計數信號量的功能大概和與互斥鎖合用的條件變量一樣強大。在很多情況下, 採用信號量的程序比採用條件變量要簡單一些(如下面的例子所示)。
然而,如果一個互斥鎖和條件變量一起使用,有一個隱含的框架,程序的哪一 部分被保護是明顯的。在信號量則不然,它可以用同時性編程當中的go to 來調用, 它更適合用於那些結構性不強的,不精確的方面。

3.4.1計數信號量

在概念上,一個信號量是一個非負整數。信號量在典型情況下用來協調資源, 信號量一般被初始化爲可用資源的數量。線程在假如資源是給計數器加1,在拿走資 源時給計數器減1,操作都具有原子性。
如果一個信號量的值變爲0,表明已無可用資源,想要給信號量減1的操作必須 等到它爲正時。
表3-4 信號量函數
函數 操作
Sema_init(3T) 初始化信號量
Sema_post(3T) 增加信號量
Sema_wait(3T) 關於信號量阻塞
Sema_trywait(3T) 減少信號量
Sema_destroy(3T) 破壞信號量的狀態
因爲信號量不被哪個線程佔有,它們可以用異步事件來通知(例如信號處理器)。 而且,因爲信號量包含狀態,他們可以被異步使用???,而不用象條件變量那樣 一定要先獲得互斥鎖。
缺省情況下,等待信號量的多個線程退出阻塞的順序是不確定的。
信號量在使用前一定要初始化。

3.4.2初始化一個信號量

sema_init(3T)
#include (or #include )
int sema_init(sema_t *sp, unsigned int count, int type, void *arg);
sema_init用count的值來初始化由sp指向的信號量。Type可以是如下值之一(arg先不談)。
USYNC_PROCESS 信號量可以在進程間進行線程同步。只有一個進程需要初始化
信號量。Arg忽略。
USYNC_THREAD 信號量只能在進程內部進行線程同步。
多個線程不能同時初始化同一個信號量。一個信號量在使用中不能被其他線程重新初始化。
返回值--sema_init()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT sp或arg指向一個非法地址。

3.4.3給信號量增值

sema_post(3T)
#include (or #include )
int sema_destroy(sema_t *sp);
用sema_post()給由sp指向的信號量原子地(表示其不可分性,下同)增1,如果有其它線程關於信號量阻塞,其中一個退出阻塞狀態。
返回值--sema_post()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT sp指向一個非法地址。

3.4.4關於一個信號量阻塞

sema_wait(3T)
#include (or #include )
int sema_wait(sema_t *sp)
用sema_wait()使得調用線程在由sp指向的信號量小於等於零時阻塞,在其大於零原子地對其進行減操作。
返回值--sema_wait()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT sp指向一個非法地址。
EINTR 等待被信號或fork()打斷。

3.4.5給信號量減值

sema_trywait(3T)
#include (or #include )
int sema_trywait(sema_t *sp)
用sema_trywait()在sp比零大時對它進行原子地減操作。是sema_wait()的非阻塞版本。
返回值--sema_trywait()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT sp指向一個非法地址。
EBUSY sp 指向的值爲零。

3.4.6清除信號量的狀態

sema_destroy(3T)
#include (or #include )
int sema_destroy(sema_t *sp)
用sema_destroy(3T)破壞與sp指向的信號量關聯的任何狀態,但空間不被釋放。
返回值--sema_destroy()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。
EINVAL 非法參數。
EFAULT sp指向一個非法地址。

3.4.7用信號量解決生產者/消費者問題

示例3-15所示的程序與條件變量的解決方案類似;兩個信號量代表空和滿的緩衝區的數目,生產者線程在沒有空緩衝區時阻塞,消費者在緩衝區全空時阻塞。
Code Example 3-15 用信號量解決的生產者/消費者問題
Typedef struct{
Char buf[BSIZE];
Sema_t occupied;
Sema_t empty;
Int nextin;
Int nextout;
Sema_t pmut;
Sema_t cmut;
} buffer_t;
buffer_t buffer;
sema_init(&buffer.occupied, 0, USYNC_THREAD, 0);
sema_init(&buffer.empty, BSIZE, USYNC_THREAD, 0);
sema_init(&buffer.pmut, 1, USYNC_THREAD, 0);
sema_init(&buffer.cmut, 1, USYNC_THREAD, 0);
buffer.nextin=buffer.nextout =0;
另外一對信號量與互斥鎖作用相同,用來在有多生產者和多個空緩衝區的情況下,或者是有多個消費者和多個滿的緩衝區的情況下控制對緩衝區的訪問。互斥鎖同樣可以工作,但這裏主要是演示信號量的例子。
Code Example 3-16 生產者/消費者問題--生產者
Void producer(buffer_t *b, char item){
Sema_wait(&b->empty);
Sema_wait(&b->pmut);
b->buf[b->nextin]=item;
b->nextin++;
b->nextin %=BSIZE;
sema_post( &b->pmut);
sema_post(&b->occupied);
}
Code Example 3-17 生產者/消費者問題--消費者
Char consumer(buffer_t *b){
Char item;
Sema_wait(&b->occupied);
Sema_wait(&b->cmut);
Item=b->buf[b->nextout];
b->nextout++;
b->nextout %=BSIZE;
sema_post (&b->cmut);
sema_post(&b->empty):
return(item);
}

3.5進程間同步

四種同步原語中的任何一種都能做進程間的同步。只要保證同步變量在共享內存 段,並且帶USYNC_PROCESS參數來對其進行初始化。在這之後,對同步變量的使用和 USYNC_THREAD初始化後的線程同步是一樣的。

Mutex_init(&m, USYNC_PROCESS,0);
Rwlock_init(&rw, USYNC_PROCESS,0);
Cond_init(&cv,USYNC_PROCESS,0);
Sema_init(&s,count,USYNC_PROCESS,0);
示例3-18顯示了一個生產者/消費者問題,生產者和消費者在兩個不同的進程裏。 主函數把全零的內存段映射到它的地址空間裏。注意mutex_init()和cond_init()一 定要用type=USYNC_PROCESS來初始化。
子進程運行消費者,父進程運行生產者。
此例也顯示了生產者和消費者的驅動程序。生產者驅動producer_driver()簡單 地從stdin中讀字符並且調用生產者函數producer()。消費者驅動consumer_driver() 通過調用consumer()來讀取字符,並將其寫入stdout。
Code Example 3-18 生產者/消費者問題,用USYNC_PROCESS
Main(){
Int zfd;
Buffer_t * buffer;
Zfd=open("/dev/zero", O_RDWR);
Buffer=(buffer_t *)mmap(NULL, sizeof(buffer_t),
PROT_READ|PROT_WRITE, MAP_SHARED, zfd, 0);
Buffer->occupied=buffer->nextin=buffer->nextout=0;
Mutex_init(&buffer->lock, USYNC_PROCESS,0);
Cond_init(&buffer->less, USYNC_PROCESS, 0);
Cond_init(&buffer->more, USYNC_PROCESS, 0);
If(fork()==0)
Consumer_driver(buffer);
Else
Producer_driver(buffer);
}
void producer_driver(buffer_t *b){
int item;
while(1){
item=getchar();
if(item==EOF){
producer(b, '');
break;
} else
producer(b, (char)item);
}
}
void consumer_driver(buffer_t *b){
char item;
while (1) {
if ((item=consumer(b))=='')
break;
putchar(item);
}
}
一個子進程被創建出來運行消費者;父進程運行生產者。

3.6同步原語的比較

Solaris中最基本的同步原語是互斥鎖。所以,在內存使用和執行時它是最 有效的。對互斥鎖最基本的使用是對資源的依次訪問。
在Solaris中效率排第二的是條件變量。條件變量的基本用法是關於一個狀態 的改變而阻塞。在關於一個條件變量阻塞之前一定要先獲得互斥鎖,在從 cond_wait()返回且改變變量狀態後一定要釋放該互斥鎖。
信號量比條件變量佔用更多的內存。因爲信號量是作用於狀態,而不是控制,所以在一些特定的條件下它更容易使用。和鎖不同,信號量沒有一個所 有者。任何線程都可以給已阻塞的信號量增值。
讀寫鎖是Solaris裏最複雜的同步機制。這意味着它不象其他原語那樣細緻。一個讀寫鎖通常用在讀操作比寫操作頻繁的時候。  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章