1.?
3 使用同步對象來編程
本章定義了四種可用的同步類型,并且討論實現同步的注意事項。
互斥鎖(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(;
{
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里最復雜的同步機制。這意味著它不象其他原語那樣細致 ???。一個讀寫鎖通常用在讀操作比寫操作頻繁的時候。
多線程編程指南1--線程基礎
線程編程指南1--線程基礎
Wednesday, 29. March 2006, 11:48:45
本文出自:BBS水木清華站 作者:Mccartney (coolcat) (2002-01-29 20:25:25)
multithreading可以被翻譯成多線程控制。與傳統的UNIX不同,一個傳統 的UNIX進程包含一個單線程,而多線程(MT)則把一個進程分成很多可執行線 程,每一個線程都獨立運行。
閱讀本章可以讓你理解:
Defining Multithreading Terms
Benefiting From Multithreading
Looking At Multithreading Structure
Meeting Multithreading Standards
因為多線程可以獨立運行,用多線程編程可以
1) 提高應用程序響應;
2) 使多CPU系統更加有效;
3) 改善程序結構;
4) 占用更少的系統資源;
5) 改善性能;
1.1定義多線程術語:
線程:在進程的內部執行的指令序列;
單線程:單線程;
多線程:多線程;
用戶級線程:在用戶空間內的由線程函數庫進程控制的現成;
輕進程:又稱LWP,內核內部的執行核代碼和系統調用的線程;
綁定(bound)線程:永遠限制在LWP內的線程;
非綁定(unbound)線程:在LWP動態捆綁和卸綁的線程;
記數信號量:一個基于內存的同步機制;
1.1.1定義同時(concurrency)和并行(parallism):
在進程內至少同時有兩個線程進行(process)時存在同時性問題;至少同時有兩個線程在執行時存在并行問題;
在單處理器上執行的多線程的進程內部,處理器可以在線程中間切換執行, 這樣實現了同時執行;在共享內存多處理器上執行的同一個多線程進程,每一 個線程可以分別在不同的處理器上進行,是為并行。
當進程里的線程數不多于處理器的數量時,線程支持系統和操作系統保 證線程在不同的處理器上執行。例如在一個m處理器和m線程運行一個矩陣乘法, 每一個線程計算一列。
1.2多線程的益處
1.2.1提高應用程序響應
任何一個包含很多互不關聯的操作(activity)的程序都可以被重新設計, 使得每一個操作成為一個線程。例如,在一個GUI(圖形用戶界面)內執行一 個操作的同時啟動另外一個,就可以用多線程改善性能。
1.2.2使多處理器效率更高
典型情況下,有同時性需求的多線程應用程序不需要考慮處理器的數量。應用程序的性能在被多處理器改善的同時對用戶是透明的。
數學計算和有高度并發性需求的應用程序,比如矩陣乘法,在多處理器平臺上可以用多線程來提高速度。
1.2.3改善程序結構
許多應用程序可以從一個單一的、巨大的線程改造成一些獨立或半獨立的 執行部分,從而得到更有效的運行。多線程程序比單線程程序更能適應用戶需 求的變更。
1.2.4占用較少的系統資源
應用程序可以通過使用兩個或更多的進程共享內存的辦法來實現多于一個 現成的控制。然而,每一個進程都要有一個完整的地址空間和操作系統狀態表 項。用于創建和維護多進程大量的狀態表的開銷與多線程方法相比,在時間上 和空間上都更為昂貴。而且,進程所固有的獨立性使得程序員花費很多精力來 實現進程間的通信和同步。
1.2.5把線程和RPC結合起來
把多線程和RPC(remote procedure call,遠程過程調用)結合起來,你可以使用沒內存共享的多處理器(比方說一個工作站組)。這種結構把這組工作站當作一個大的多處理器系統,使應用程序分布得更加容易。
例如,一個線程可以創建子線程,每一個子進程可以做RPC,調用另外一 臺機器上的過程。盡管最早的線程僅僅創建一些并行的線程,這種并行可以包 括多臺機器的運行。
1.2.6提高性能
本部分的性能數據是從SPARC station2(Sun 4/75)上采集的。測量精度為微秒。
1. 線程創建時間
表1-1顯示了使用thread package做緩存的缺省堆棧來創建線程的時 間。時間的測量僅僅包括實際的生成時間。不包括切換到線程的時間。比 率(ratio)列給出了該行生成時間與前一行的比。 數據表明,線程是更加經濟的。創建一個新進程大概是創建一個 unbound線程的30倍,是創建一個包含線程和LWP的bound線程的5倍。
Table 1-1 Thread Creation Times
Operation Microseconds Ritio
Create unbound thread 52 -
Create bound thread 350 6.7
Fork() 1700 32.7
2. 線程同步(synchronization)時間
表1-2列出了兩個線程使用pv操作的同步時間。
Table 1-2 Thread Synchronization Times
Operation Microseconds Ratio
Unbound thread 66 -
Bound thread 390 5.9
Between Processes 200 3
1.3多線程結構一覽
傳統的UNIX支持現成概念--每一個進程包含一個單線程,所以用多進程就 是使用多線程。但是一個進程還有一個地址空間,創建一個新進程意味著需要 創建一個新的地址空間。
因此,創建一個進程是昂貴的,而在一個已經存在的進程內部創建線程是 廉價的。創建一個線程的時間比創建一個進程的時間要少成千倍,部分是因為 在線程間切換不涉及地址空間的切換。
在進程內部的線程間通信很簡單,因為線程們共享所有的東西--特別是地址空間。所以被一個線程生成的數據可以立刻提供給其他線程。
支持多線程的接口(界面)是通過一個函數庫libthread實現的。多線程通過把內核級資源和用戶級資源獨立開來提供了更多的靈活性。
1.3.1用戶級線程
線程僅僅在進程內部是可見的,在進程內部它們共享諸如地址空間、已經 打開的文件等所有資源。以下的狀態是線程私有的,即每一個線程的下列狀態 在進程內部是唯一的。
.線程號(Thread ID)
.寄存器狀態(包括程序計數器和堆棧指針)
.堆棧
.信號掩碼(Signal mask)
.優先級(Priority)
.線程私有的存儲段(Thread-private storage)
因為線程共享進程的執行代碼和大部分數據,共享數據被一個線程修改之 后可以進程內的其他線程見到。當一個進程內部線程與其他線程通信的時候, 可以不經過操作系統。
線程是多線程編程的主要主要借口。用戶級的線程可以在用戶空間操作, 從而避免了與內核之間的互相切換。一個應用程序可以擁有幾千個線程而不占 用太多的內核資源。占用內核資源的多少主要決定于應用程序本身。
在缺省情況下,線程是非常輕便的。但是,為了控制一個線程(例如,更 多地控制進程調度策略),應用程序應當綁定線程。當一個應用程序把線程的所 有執行資源綁定后,線程就變成了內核資源(參見第9頁"bound 線程")。 總之,solaris用戶級線程是:
.創建的低開銷,因為只在運行是占用用戶地址空間的虛擬內存的幾個bit。
.快速同步,因為同步是在用戶級進行,不需要接觸到內核級。
.可以通過線程庫libthread很容易地實現。
圖1-1 多線程系統結構(略)
1.3.2輕進程(Lightweight Porcesses:LWP)
線程庫采用內核支持的稱為輕進程的底層控制線程。你可以把LWP看作一個 可以執行代碼和系統調用的虛擬的CPU。
大多數程序員使用線程是并不意識到LWP的存在。下面的內容僅僅幫助理解 bound和unbound線程之間的區別。
------------------------------------
NOTE:Solaris2.x的LWP不同于SunOs4.0的LWP庫,后者在solaris2.x中不再被支持。
------------------------------------
類似于在stdio中fopen和fread調用open和read,線程接口調用LWP接口, 原因是一樣的。
LWP建立了從用戶級到內核級的橋梁。每個進程包含了一個或更多LWP,每個 LWP運行著一個或多個用戶線程。創建一個現成通常只是建立一個用戶環境 (context),而不是創建一個LWP。
在程序員和操作系統的精心設計下,用戶級線程庫保證了可用的LWP足夠驅動 當前活動的用戶級線程。但是,用戶線程和LWP之間不是一一對應的關系,用戶級 線程可以在LWP之間自由切換。
程序員告訴線程庫有多少線程可以同時"運行"。例如,如果程序員指定最多有 三個線程可以同時運行,至少要有3個可用的LWP。如果有三個可用的處理器,線程 將并行進行。如果這里只有一個處理器,操作系統將在一個處理器上運行三個LWP。 如果所有的LWP阻塞,線程庫將在緩沖池內增加一個LWP。
當一個用戶線程由于同步原因而阻塞,它的LWP將移交給下一個可運行的線程。 這種移交是通過過程間的連接(coroutine linkage),而不是做系統調用而完成。
操作系統決定哪一個LWP什么時候在哪一個處理器上運行。它不考慮進程中線 程的類型和數量。內核按照LWP的類型和優先級來分配CPU資源。線程庫按照相同 的方法來為線程分配LWP。每個LWP被內核獨立地分發,執行獨立的系統調用,引 起獨立的頁錯誤,而且在多處理器的情況下將并行執行。
一些特殊類型的LWP可以不被直接交給線程。(!?不明)
1.3.3非綁定線程Unbound Threads
在LWP緩沖池中排隊的線程稱為unbound thread。通常情況下我們的線程都是 unbound的,這樣他們可以在LWP之間自由切換。
線程庫在需要的時候激活LWP并把它們交給可以執行的線程。LWP管理線程的 狀態,執行線程的指令。如果線程在同步機制中被阻塞,或者其他線程需要運行, 線程狀態被存在進程內存中,LWP被移交給其他線程。
1.3.4綁定線程Bound Threads
如果需要,你可以將一個線程綁定在某個LWP上。
例如,你可以通過綁定一個線程來實現:
1. 將線程全局調度(例如實時)
2. 使線程擁有可變的信號棧
3. 給線程分配獨立的定時器和信號(alarm)
在線程數多于LWP時,bounded比unbound線程體現出一些優越性。
例如,一個并行的矩陣計算程序在每個線程當中計算每一行。如果每個處理器 都有一個LWP,但每個LWP都要處理多線程,每個處理器將要花費相當的時間來切換 線程。在這種情況下,最好使每個LWP處理一個線程,減少線程數,從而減少線程 切換。
在一些應用程序中,混合使用bound和unbound線程更加合適。
例如,有一個實時的應用程序,希望某些線程擁有全局性的優先級,并被實時 調度,其他線程則轉入后臺計算。另一個例子是窗口系統,大多數操作都是 unbound的,但鼠標操作需要占用一個高優先級的,bound的,實時的線程。
1.4多線程的標準
多線程編程的歷史可以回溯到二十世紀60年代。在UNIX操作系統中的發展是從 80年代中期開始的。也許是令人吃驚的,關于支持多線程有很好的協議,但是今 天我們仍然可以看到不同的多線程開發包,他們擁有不同的接口。
但是,某幾年里一個叫做POSIX1003.4a的小組研究多線程編程標準。當標準完 成后,大多數支持多線程的系統都支持POSIX接口。很好的改善了多線程編程的可 移植性。
solaris多線程支持和POSIX1003.4a沒有什么根本性的區別。雖然接口是不同 的,但每個系統都可以容易地實現另外一個系統可以實現的任何功能。它們之間沒 有兼容性問題,至少solaris支持兩種接口。即使是在同一個應用程序里,你也可 以混合使用它們。
用solaris線程的另一個原因是使用支持它的工具包,例如多線程調試工具 (multighreaded debugger)和truss(可以跟蹤一個程序的系統調用和信號), 可以很好地報告線程的狀態。
multithreading可以被翻譯成多線程控制。與傳統的UNIX不同,一個傳統 的UNIX進程包含一個單線程,而多線程(MT)則把一個進程分成很多可執行線 程,每一個線程都獨立運行。
閱讀本章可以讓你理解:
Defining Multithreading Terms
Benefiting From Multithreading
Looking At Multithreading Structure
Meeting Multithreading Standards
因為多線程可以獨立運行,用多線程編程可以
1) 提高應用程序響應;
2) 使多CPU系統更加有效;
3) 改善程序結構;
4) 占用更少的系統資源;
5) 改善性能;
1.1定義多線程術語:
線程:在進程的內部執行的指令序列;
單線程:單線程;
多線程:多線程;
用戶級線程:在用戶空間內的由線程函數庫進程控制的現成;
輕進程:又稱LWP,內核內部的執行核代碼和系統調用的線程;
綁定(bound)線程:永遠限制在LWP內的線程;
非綁定(unbound)線程:在LWP動態捆綁和卸綁的線程;
記數信號量:一個基于內存的同步機制;
1.1.1定義同時(concurrency)和并行(parallism):
在進程內至少同時有兩個線程進行(process)時存在同時性問題;至少同時有兩個線程在執行時存在并行問題;
在單處理器上執行的多線程的進程內部,處理器可以在線程中間切換執行, 這樣實現了同時執行;在共享內存多處理器上執行的同一個多線程進程,每一 個線程可以分別在不同的處理器上進行,是為并行。
當進程里的線程數不多于處理器的數量時,線程支持系統和操作系統保 證線程在不同的處理器上執行。例如在一個m處理器和m線程運行一個矩陣乘法, 每一個線程計算一列。
1.2多線程的益處
1.2.1提高應用程序響應
任何一個包含很多互不關聯的操作(activity)的程序都可以被重新設計, 使得每一個操作成為一個線程。例如,在一個GUI(圖形用戶界面)內執行一 個操作的同時啟動另外一個,就可以用多線程改善性能。
1.2.2使多處理器效率更高
典型情況下,有同時性需求的多線程應用程序不需要考慮處理器的數量。應用程序的性能在被多處理器改善的同時對用戶是透明的。
數學計算和有高度并發性需求的應用程序,比如矩陣乘法,在多處理器平臺上可以用多線程來提高速度。
1.2.3改善程序結構
許多應用程序可以從一個單一的、巨大的線程改造成一些獨立或半獨立的 執行部分,從而得到更有效的運行。多線程程序比單線程程序更能適應用戶需 求的變更。
1.2.4占用較少的系統資源
應用程序可以通過使用兩個或更多的進程共享內存的辦法來實現多于一個 現成的控制。然而,每一個進程都要有一個完整的地址空間和操作系統狀態表 項。用于創建和維護多進程大量的狀態表的開銷與多線程方法相比,在時間上 和空間上都更為昂貴。而且,進程所固有的獨立性使得程序員花費很多精力來 實現進程間的通信和同步。
1.2.5把線程和RPC結合起來
把多線程和RPC(remote procedure call,遠程過程調用)結合起來,你可以使用沒內存共享的多處理器(比方說一個工作站組)。這種結構把這組工作站當作一個大的多處理器系統,使應用程序分布得更加容易。
例如,一個線程可以創建子線程,每一個子進程可以做RPC,調用另外一 臺機器上的過程。盡管最早的線程僅僅創建一些并行的線程,這種并行可以包 括多臺機器的運行。
1.2.6提高性能
本部分的性能數據是從SPARC station2(Sun 4/75)上采集的。測量精度為微秒。
1. 線程創建時間
表1-1顯示了使用thread package做緩存的缺省堆棧來創建線程的時 間。時間的測量僅僅包括實際的生成時間。不包括切換到線程的時間。比 率(ratio)列給出了該行生成時間與前一行的比。 數據表明,線程是更加經濟的。創建一個新進程大概是創建一個 unbound線程的30倍,是創建一個包含線程和LWP的bound線程的5倍。
Table 1-1 Thread Creation Times
Operation Microseconds Ritio
Create unbound thread 52 -
Create bound thread 350 6.7
Fork() 1700 32.7
2. 線程同步(synchronization)時間
表1-2列出了兩個線程使用pv操作的同步時間。
Table 1-2 Thread Synchronization Times
Operation Microseconds Ratio
Unbound thread 66 -
Bound thread 390 5.9
Between Processes 200 3
1.3多線程結構一覽
傳統的UNIX支持現成概念--每一個進程包含一個單線程,所以用多進程就 是使用多線程。但是一個進程還有一個地址空間,創建一個新進程意味著需要 創建一個新的地址空間。
因此,創建一個進程是昂貴的,而在一個已經存在的進程內部創建線程是 廉價的。創建一個線程的時間比創建一個進程的時間要少成千倍,部分是因為 在線程間切換不涉及地址空間的切換。
在進程內部的線程間通信很簡單,因為線程們共享所有的東西--特別是地址空間。所以被一個線程生成的數據可以立刻提供給其他線程。
支持多線程的接口(界面)是通過一個函數庫libthread實現的。多線程通過把內核級資源和用戶級資源獨立開來提供了更多的靈活性。
1.3.1用戶級線程
線程僅僅在進程內部是可見的,在進程內部它們共享諸如地址空間、已經 打開的文件等所有資源。以下的狀態是線程私有的,即每一個線程的下列狀態 在進程內部是唯一的。
.線程號(Thread ID)
.寄存器狀態(包括程序計數器和堆棧指針)
.堆棧
.信號掩碼(Signal mask)
.優先級(Priority)
.線程私有的存儲段(Thread-private storage)
因為線程共享進程的執行代碼和大部分數據,共享數據被一個線程修改之 后可以進程內的其他線程見到。當一個進程內部線程與其他線程通信的時候, 可以不經過操作系統。
線程是多線程編程的主要主要借口。用戶級的線程可以在用戶空間操作, 從而避免了與內核之間的互相切換。一個應用程序可以擁有幾千個線程而不占 用太多的內核資源。占用內核資源的多少主要決定于應用程序本身。
在缺省情況下,線程是非常輕便的。但是,為了控制一個線程(例如,更 多地控制進程調度策略),應用程序應當綁定線程。當一個應用程序把線程的所 有執行資源綁定后,線程就變成了內核資源(參見第9頁"bound 線程")。 總之,solaris用戶級線程是:
.創建的低開銷,因為只在運行是占用用戶地址空間的虛擬內存的幾個bit。
.快速同步,因為同步是在用戶級進行,不需要接觸到內核級。
.可以通過線程庫libthread很容易地實現。
圖1-1 多線程系統結構(略)
1.3.2輕進程(Lightweight Porcesses:LWP)
線程庫采用內核支持的稱為輕進程的底層控制線程。你可以把LWP看作一個 可以執行代碼和系統調用的虛擬的CPU。
大多數程序員使用線程是并不意識到LWP的存在。下面的內容僅僅幫助理解 bound和unbound線程之間的區別。
------------------------------------
NOTE:Solaris2.x的LWP不同于SunOs4.0的LWP庫,后者在solaris2.x中不再被支持。
------------------------------------
類似于在stdio中fopen和fread調用open和read,線程接口調用LWP接口, 原因是一樣的。
LWP建立了從用戶級到內核級的橋梁。每個進程包含了一個或更多LWP,每個 LWP運行著一個或多個用戶線程。創建一個現成通常只是建立一個用戶環境 (context),而不是創建一個LWP。
在程序員和操作系統的精心設計下,用戶級線程庫保證了可用的LWP足夠驅動 當前活動的用戶級線程。但是,用戶線程和LWP之間不是一一對應的關系,用戶級 線程可以在LWP之間自由切換。
程序員告訴線程庫有多少線程可以同時"運行"。例如,如果程序員指定最多有 三個線程可以同時運行,至少要有3個可用的LWP。如果有三個可用的處理器,線程 將并行進行。如果這里只有一個處理器,操作系統將在一個處理器上運行三個LWP。 如果所有的LWP阻塞,線程庫將在緩沖池內增加一個LWP。
當一個用戶線程由于同步原因而阻塞,它的LWP將移交給下一個可運行的線程。 這種移交是通過過程間的連接(coroutine linkage),而不是做系統調用而完成。
操作系統決定哪一個LWP什么時候在哪一個處理器上運行。它不考慮進程中線 程的類型和數量。內核按照LWP的類型和優先級來分配CPU資源。線程庫按照相同 的方法來為線程分配LWP。每個LWP被內核獨立地分發,執行獨立的系統調用,引 起獨立的頁錯誤,而且在多處理器的情況下將并行執行。
一些特殊類型的LWP可以不被直接交給線程。(!?不明)
1.3.3非綁定線程Unbound Threads
在LWP緩沖池中排隊的線程稱為unbound thread。通常情況下我們的線程都是 unbound的,這樣他們可以在LWP之間自由切換。
線程庫在需要的時候激活LWP并把它們交給可以執行的線程。LWP管理線程的 狀態,執行線程的指令。如果線程在同步機制中被阻塞,或者其他線程需要運行, 線程狀態被存在進程內存中,LWP被移交給其他線程。
1.3.4綁定線程Bound Threads
如果需要,你可以將一個線程綁定在某個LWP上。
例如,你可以通過綁定一個線程來實現:
1. 將線程全局調度(例如實時)
2. 使線程擁有可變的信號棧
3. 給線程分配獨立的定時器和信號(alarm)
在線程數多于LWP時,bounded比unbound線程體現出一些優越性。
例如,一個并行的矩陣計算程序在每個線程當中計算每一行。如果每個處理器 都有一個LWP,但每個LWP都要處理多線程,每個處理器將要花費相當的時間來切換 線程。在這種情況下,最好使每個LWP處理一個線程,減少線程數,從而減少線程 切換。
在一些應用程序中,混合使用bound和unbound線程更加合適。
例如,有一個實時的應用程序,希望某些線程擁有全局性的優先級,并被實時 調度,其他線程則轉入后臺計算。另一個例子是窗口系統,大多數操作都是 unbound的,但鼠標操作需要占用一個高優先級的,bound的,實時的線程。
1.4多線程的標準
多線程編程的歷史可以回溯到二十世紀60年代。在UNIX操作系統中的發展是從 80年代中期開始的。也許是令人吃驚的,關于支持多線程有很好的協議,但是今 天我們仍然可以看到不同的多線程開發包,他們擁有不同的接口。
但是,某幾年里一個叫做POSIX1003.4a的小組研究多線程編程標準。當標準完 成后,大多數支持多線程的系統都支持POSIX接口。很好的改善了多線程編程的可 移植性。
solaris多線程支持和POSIX1003.4a沒有什么根本性的區別。雖然接口是不同 的,但每個系統都可以容易地實現另外一個系統可以實現的任何功能。它們之間沒 有兼容性問題,至少solaris支持兩種接口。即使是在同一個應用程序里,你也可 以混合使用它們。
用solaris線程的另一個原因是使用支持它的工具包,例如多線程調試工具 (multighreaded debugger)和truss(可以跟蹤一個程序的系統調用和信號), 可以很好地報告線程的狀態。
2.?
多線程編程指南2--用多線程編程
Wednesday, 29. March 2006, 11:50:40
本文出自:BBS水木清華站 作者:Mccartney (coolcat) (2002-01-29 20:26:32)
2 用多線程編程
2.1線程(函數)庫(The Threads Library)
用戶級多線程是通過線程庫,libthread來實現的(參考手冊第3頁: library routines)。線程庫支持信號,為可運行的程序排隊,并負責同 時操縱多任務。
這一章討論libthread中的一些通用過程,首先接觸基本操作,然后循 序漸進地進入更復雜的內容。
創建線程-基本特性 Thr_create(3T)
獲得線程號 Thr_self(3T)
執行線程 Thr_yield(3T,the below is same)
掛起或繼續線程 Thr_suspend
Thr_continue
向線程送信號 Thr_kill
設置線程的調用掩模 Thr_sigsetmask
終止線程 Thr-exit
等待線程終止 Thr-join
維護線程的私有數據 Thr_keycreate
Thr_setspecific
Thr_getspecific
創建線程-高級特性 Thr_create
獲得最小堆棧容量 Thr_min_stack
獲得或設置線程的同時性等級 Thr_getconcurrency
Thr_setconcurrency
獲得或設置線程的優先級 Thr_getprio
Thr_setprio
2.1.1創建線程-基本篇
thr_create過程是線程庫所有過程當中最復雜的一個。這部分的內容僅適用于你使用thr_create的缺省參數來創建進程。
對于thr_create更加復雜的使用,包括如何使用自定參數,我們將在高級特性部分給出說明。
thr_create(3T)
這個函數用于在當前進程中添加一個線程。注意,新的線程不繼承未處理的信號,但繼承優先級和信號掩模。
#include
int thr_create(void *stack_base,size_t stack_size,
void *(*start_routine) (void*),void *arg,long flags,
thread_t *new_thread);
size_t thr_min_stack(void);
stack_base--新線程的堆棧地址。如果stack_base是空則thr_create()按 照stack_size為新線程分配一個堆棧。
Stack_size--新線程堆棧的字節數。如果本項為0,將使用缺省值,一般 情況下最好將此項設為0。并不是每個線程都需要指定堆棧空間。線程庫為每個線程的堆棧分配1M 的虛擬內存,不保留交換空間。(線程庫用mmap(2)的MAP_NORESERVE的選項 來實現這種分配)。
Start_routine--指定線程開始執行的函數。如果start_routine返回, 線程將用該函數的返回值作為退出狀態而退出。(參考thr_exit(3T))。
Flags--指定新線程的屬性,一般設置為0。
Flags的值是通過下列內容的位同或來實現的(最后四個flags在高級特性中給出)。
1. THR_DETACHED 將新線程分離,使得它的線程號和其他資源在線程 結束時即可以回收利用。當你不想等待線程終止時,將其置位。如果沒有明確的 同步需求阻礙,一個不掛起的,分離的線程可以在創建者的thr_create返回之前 終止并將其線程號分配給一個心得線程。
2. THR_SUSPENDED掛起新線程,直到被thr_continue喚醒。
3. THR_BOUND把新線程永久綁定在一個LWP上(生成一個綁定線程)。
4. THR_NEW_LWP將非綁定線程的同時性級別加1。
5. THR_DAEMON新線程為一個守護線程。
New_thread--指向存儲新線程ID的地址。多數情況下設置為0。
Return Values--thr_create()在成功執行后返回0并退出。任何其他返回值表明有錯誤發生。當以下情況被檢測到時,thr_create()失敗并返回響應的值。
EAGAIN :超出了系統限制,例如創建了太多的LWP。
ENOMEM:可用內存不夠創建新線程。
EINVAL:stack_base不是NULL而且stack_size比thr_minstack()函數返回的最小堆棧要小。
2.1.2獲取線程號
thr_self(3T) 獲得自身的線程號。
#include
thread_t thr_self(void)
返回值--調用者的線程號。
2.1.3放棄執行
thr_yield(3T)
thr_yield停止執行當前線程,將執行權限讓給有相同或更高優先權的線程。
#include
void thr_yield(void);
2.1.4掛起或繼續執行線程
thr_suspend(3T) 掛起線程。
#include
int thr_suspend(thread_t target_thread);
thr_suspend()立即掛起由target_thread指定的線程。在thr_suspend成功返回后,掛起的線程不再執行。后繼的thr_suspend無效。
Return Values--執行成功后返回0。其他返回值意味著錯誤。以下情況發生時,thr_suspend()失敗并返回相關值。
ESRCH: 在當前進程中找不到target_thread。
Thr_continue(3T)
Thr_continue()恢復執行一個掛起的線程。一旦線程脫離掛起狀態,后繼的
thr_continue將無效。
#include
int thr_continue(thread_t target_thread);
一個掛起的線程不會被信號喚醒。信號被掛起知道線程被thr-continue恢復執行。
返回值--成功執行后返回0。其他值意味著錯誤。在以下情況發生時,函數失敗并返回相關值。
ESRCH:target_thread在當前進程中找不到。
2.1.5向線程發信號
thr_kill(3T)向線程發信號
#include
#include
int thr_kill(thread_t target_thread,int sig);
thr_kill向線程號為target_thread的線程發送信號sig。Target_thread一定要與調用線程處于同一個進程內。參數sig一定是signal(5)中定義過的。
當sig是0時,錯誤檢查將被執行,沒有實際的信號被發送。這可以用來檢測arget_thread參數是否合法。
返回值--成功執行后返回0,其他值意味著錯誤。在以下情況發生時,函數失敗并返回相關值。
EINVAL:sig非法;
ESRCH:target_thread找不到;
2.1.6設置本線程的信號掩模
thr_sigsetmask(3T) 獲取或改變本線程的信號掩模(signal mask)
#include
#include
int thr_sigsetmask(int how,const sigset_t *set,sigset_t *oset);
how參數決定信號設置將被如何改變,可以是下列值之一:
SIG_BLOCK--在當前信號掩模上增加set,set指要阻塞的信號組。
SIG_UNBLOCK--在當前信號掩模上去掉set,set指要解除阻塞的信號組。
SIG_SETMASK--用新的掩模代替現有掩模,set指新的信號掩模。
當set的值是NULL時,how的值并不重要,信號掩模將不被改變。所以,要查詢當前的信號掩模,就給set賦值為NULL。
當參數oset不是NULL時,它指向以前的信號掩模存放的地方。
Return Values--正常執行后返回0。其他值意味著錯誤。在以下情況發生時, 函數失敗并返回相關值。
EINVAL:set不是NULL且how沒有被定義;
EFAULT:set或oset不是合法地址;
2.1.7終止線程
thr_exit(3T)
用來終止一個線程。
#include
void thr_exit(void *status);
thr_exit 函數終止當前線程。所有的私有數據被釋放。如果調用線程不是一個分離線程,線程的ID和返回狀態保留直到有另外的線程在等待。否則返回狀態被忽略,線程號被立刻重新使用。
返回值--當調用線程是進程中的最后一個非守護線程,進程將用狀態0退出。 當最初的線程從main()函數中返回時進程用該線程main函數的返回值退出。
線程可以通過兩種方式停止執行。第一種是從最初的過程中返回。第二種是 提供一個退出代碼,通過調用thr_exit()結束。下面的事情依賴于在線程創建時 flags的設置。
線程A終止的缺省操作(當flags的相應位設為0時,執行缺省操作)是保持 狀態,直到其它線程(不妨設為B)通過"聯合"的方式得知線程A已經死亡。聯合 的結果是B線程得到線程A的退出碼,A自動消亡。你可以通過位或來給flags的 THR_DETACHED參數置位,使得線程在thr_exit()之后或從最初過程返回后立即消 亡。在這種情況下,它的退出碼不會被任何線程獲得。
有一個重要的特殊情況,在主線程--即最初存在的線程--從主函數返回或調 用了exit(),整個進程將終止。所以在主線程中要注意不要過早地從主函數main 返回。
如果主線程僅僅調用了thr_exit(),僅僅是它自己死亡,進程不會結束,進 程內的其他線程將繼續運行(當然,如果所有的線程都結束,進程也就結束了)。
如果一個線程是非分離的,在它結束后一定要有其它進程與它"聯合",否則 該線程的資源就不會被回收而被新線程使用。所以如果你不希望一個線程被 "聯合",最好按照分離線程來創建。
另外一個flag參數是THR_DAEMON。使用這個標志創建的線程是守護線程,在 其他線程終止之后,這些線程自動終止。這些守護線程在線程庫內部特別有用。 守護線程可以用庫內函數創建--在程序的其他部分是不可見的。當程序中所 有的其他線程終止,這些線程自動終止。如果它們不是守護線程,在其它線程終 止后他們不會自動終止,進程不會自動結束。
2.1.8等待線程結束
thr_join(3T) 用thr_join函數來等待線程終止。
#include
int thr_join(thread_t wait_for,thread_t *departed,void **status);
thr_join()函數阻塞自身所在的線程,直到由wait_for指定的線程終止。指 定的線程一定與本線程在同一個進程內部,而且一定不是分離線程。當wait_for 參數為0時,thr_join等待任何一個非分離線程結束。換句話說,當不指定線程 號時,任何非分離線程的退出將導致thr_join()返回。
當departed參數不是NULL時,在thr_join正常返回時它指向存放終止線程ID 的地址。當status參數不是NULL時,在thr_join正常返回時它指向存放終止線程 退出碼的地址。
如果線程創建時指定了堆棧,在thr_join返回時堆棧可以被回收。由它返回 的線程號可以被重新分配。
不能有兩個線程同時等待同一個線程,如果出現這種情況,其中一個線程正 常返回,另外一個返回ESRCH錯誤。
返回值--thr_join()在正常執行后返回0,其他值意味著錯誤。在以下情況 發生時,函數失敗并返回相關值。
ESRCH wait_for不合法,等待的線程為分離現成。
EDEADLK 等待自身結束。
最后步驟
thr_join()有三個參數,提供了一定的靈活性。當你需要一個線程等待 直到另外一個指定的線程結束,應當把后者的ID提供為第一參數。如果 需要等待到任何其他的線程結束,將第一參數置零。
如果調用者想知道是那個線程終止,第二參數應當是儲存死線程的ID的地址。 如果不感興趣,將該參數置零。最后如果需要知道死線程的退出碼,應當指出接 收該錯誤碼的地址。
一個線程可以通過以下的代碼等待所有的非守護線程結束:
while(thr_join(0,0,0)==0)
第三個參數的聲明(void **)看上去很奇怪。相應的thr_exit()的參數為 void *。這樣做的意圖在于你的錯誤代碼為定長的四字節,c語言給定長4字節的 定義不能是void型,因為這以為著沒有參數。所以用void*。因為thr_join()的 第三參數必須是一個指向thr_exit()返回值的指針,所以類型必須是void **。
注意,thr_join()只在目標線程為非分離時有效。如果沒有特殊的同步要求 的話,線程一般都設置成分離的。
可以認為,分離線程是通常意義下的線程,而非分離線程知識特殊情況。
2.1.9簡單的例程
在例子2-1里,一個運行在頂部的線程,創建一個輔助線程來執行fetch過程, 這個輔助過程涉及到復雜的數據庫查詢,需要較長的時間。主線程在等待結果的 時候還有其他事情可做。所以它通過執行thr_join()來等待輔助過程結束。
操作結果被當作堆棧參數傳送,因為主線程等待spun-off線程結束。在一般 意義上,用malloc()存儲數據比通過線程的堆棧來存儲要好一些。????
Code Example 2-1 A Simple Threads Program
Void mainline(){
Char int result;
Thread_t helper;
Int status;
Thr_create(0,0,fetch,&result,0,&helper);
/* do something else for a while */
Thr_join(helper,0,&status);
/* it's now safe to use result*/
}
void fetch(int * result){
/*fetch value from a database */
*result=value;
thr_exit(0);
}
2.1.10維護線程專有數據
單線程C程序有兩種基本數據--本地數據和全局數據。多線程C程序增加了 一個特殊類型--線程專有數據(TSD)。非常類似與全局數據,只不過它是線程 私有的。
TSD是以線程為界限的。TSD是定義線程私有數據的唯一方法。每個線程專有 數據項都由一個進程內唯一的關鍵字(KEY)來標識。用這個關鍵字,線程可以 來存取線程私有的數據。
維護TSD的方法通過以下三個函數進行:
· thr_keycreate()--創建關鍵字
· thr_setspecific()--將一個線程綁定在一個關鍵字上
· thr_getspecific()--存儲指定地址的值
2.1.10.1 thr_keycreate(3T)
thr_keycreate()在進程內部分配一個標識TSD的關鍵字。關鍵字是進程內部唯一的,所有線程在創建時的關鍵字值是NULL。
一旦關鍵字被建立,每一個線程可以為關鍵字綁定一個值。這個值對于綁定的線程來說是唯一的,被每個線程獨立維護。
#include
int thr_keycreate(thread_key_t keyp,
void (*destructor)(void *value);
如果thr_keycreate()成功返回,分配的關鍵字被存儲在由keyp指向的區 域里。調用者一定要保證存儲和對關鍵字的訪問被正確地同步。
一個可選的析構函數,destructor,可以和每個關鍵字聯系起來。如果一 個關鍵字的destructor不空而且線程給該關鍵字一個非空值,在線程退出時該 析構函數被調用,使用當前的綁定值。對于所有關鍵字的析構函數執行的順序 是不能指定的。
返回值--thr_keycreate()在正常執行后返回0,其他值意味著錯誤。在以 下情況發生時,函數失敗并返回相關值。
EAGAIN 關鍵字的名字空間用盡
ENOMEM 內存不夠
2.1.10.2 Thr_setspecific(3T)
#include
int thr_setspecific(thread_key_t key,void *value);
thr_setspecific()為由key指定的TSD關鍵字綁定一個與本線程相關的值。 返回值--thr_setspecific在正常執行后返回0,其他值意味著錯誤。在以 下情況發生時,函數失敗并返回相關值。
ENOMEM 內存不夠
EINVAL 關鍵字非法
2.1.10.3 Thr_getspecific(3T)
#include
int thr_getspecific(thread_key_t key,void **valuep);
thr_getspecific()將與調用線程相關的關鍵字的值存入由valuep指定的區
域。
返回值--thr_getspecific()在正常執行后返回0,其他值意味著錯誤。在 以下情況發生時,函數失敗并返回相關值。
EINVAL 關鍵字非法。
2.1.10.5 全局和私有的線程專有數據
例程2-2是從一個多線程程序中摘錄出來的。這段代碼可以被任意數量的線 程執行,但一定要參考兩個全局變量:errno和mywindow,這兩個值是因線程而 異的,就是說是線程私有的。
Code Example 2-2 線程專有數據--全局且私有的
Body(){
……
while(srite(fd,buffer,size)==-1){
if(errno!=EINTR){
fprintf(mywindow,"%s/n",strerror(errno));
exit(1);
}
}
………
}
本線程的系統錯誤代碼errno可以通過線程的系統調用來獲得,而不是通過 其他線程。所以一個線程獲得的錯誤碼與其他線程是不同的。
變量mywindow指向一個線程私有的輸入輸出流。所以,一個線程的mywindow 和另外一個線程是不同的,因而最終體現在不同的窗口里。唯一的區別在于線程 庫來處理errno,而程序員需要精心設計mywindow。
下面一個例子說明了mywindow的設計方法。處理器把mywindow的指針轉換成為對_mywindow過程的調用。
然后調用thr_getspecific(),把全程變量mywindow_key和標識線程窗口的輸出參數win傳遞給它。
Code Example 2-3 將全局參考轉化為私有參考
#define mywindow _mywindow()
thread_key_t mywindow_key;
FILE * _mywindow(void){
FILE *win;
Thr_getspecific(mywindow_key,&win);
Return(win);
}
void thread_start(…){
…
make_mywindow();
…
}
變量mywindow標識了一類每個線程都有私有副本的變量;就是說,這些變量 是線程專有數據。每個線程調用make_mywindow()來初始化自己的窗口,并且生 成一個指向它的實例mywindow。 一旦過程被調用,現成可以安全地訪問mywindow,在_mywindow函數之后,線 程可以訪問它的私有窗口。所以,對mywindow的操作就象是直接操作線程私有 數據一樣。
Code Example 2-4 顯示了怎樣設置
Code Example 2-4 初始化TSD
Void make_mywindow(void){
FILE **win;
Static int once=0;
Static mutex_t lock;
Mutex_lock(&lock);
If (!once){
Once=1;
Thr_keycreate(&mywindow_key,free_key);
}
mutext_unlock(&lock);
win=malloc(sizeof(*win));
create_window(win,…);
thr_setspecific(mywindow_key,win);
}
void freekey(void *win){
free(win);
}
首先,給關鍵字mywindow_key賦一個唯一的值。這個關鍵字被用于標識 TSD。所以,第一個調用make_mywindow的線程調用thr_keycreate(),這個函 數給其第一個參數賦一個唯一的值。第二個參數是一個析構函數,用來在線程 終止后將TSD所占的空間回收。
下一步操作是給調用者分配一個TSD的實例空間。分配空間以后,調用 create_window過程,為線程建立一個窗口并用win來標識它。最后調用 thr_setspecific(),把win(即指向窗口的存儲區)的值與關鍵字綁在一起。
做完這一步,任何時候線程調用thr_getspecific(),傳送全局關鍵字, 它得到的都是該線程在調用thr_setspecific時與關鍵字綁定的值。 如果線程結束,在thr_keycreate()中建立的析構函數將被調用,每個析構 函數只有在終止的線程用thr_setspecific()為關鍵字賦值之后才會執行。
2.1.11創建線程--高級特性
2.1.11.1 thr_create(3T)
#include
int thr_create(void *stack_base,size_t stack_size,
void *(*start_routine)(void *),void * arg,
long flags,thread_t *newthread);
size_t thr_min_stack(void);
stack_base--新線程所用的堆棧地址。如果本參數為空,thr_create為新線程分配一個至少長stack_size的堆棧。
Stack_size--新線程使用堆棧的字節數。如果本參數為零,將使用缺省值。如果非零,一定要比調用thr_min_stack()獲得的值大。
一個最小堆棧也許不能容納start_routine需要的堆棧大小,所以如果 stack_size被指定,一定要保證它是最小需求與start_routine及它所調用的 函數需要的堆棧空間之和。
典型情況下,由thr_create()分配的線程堆棧從一個頁邊界開始,到離指 定大小最接近的頁邊界結束。在堆棧的頂部放置一個沒有訪問權限的頁,這樣, 大多數堆棧溢出錯誤發生在向越界的線程發送SIGSEGV信號的時候。由調用者分 配的線程堆棧 are used as is . ????
如果調用者使用一個預分配的堆棧,在指向該線程的thr_join()函數返回 之前,堆棧將不被釋放,即使線程已經終止。然后線程用該函數的返回值作為 退出碼退出。
通常情況下,你不需要為線程分配堆棧空間。線程庫為每個線程的堆棧分 配一兆的虛擬內存,不保留交換空間(線程庫用mmap(2)的MAP_NORESERVE選項 來進行分配)。
每個用線程庫創建的線程堆棧有一個"紅區"。線程庫將一個紅區放置在堆 棧頂部來檢測溢出。該頁是沒有訪問權限的,在訪問時將導致一個頁錯誤。紅 區被自動附加在堆棧頂端,不管是用指定的容量還是缺省的容量。
只有在你絕對確信你給的參數正確之后才可以指定堆棧。沒有多少情況需 要去指定堆棧或它的大小。即使是專家也很難知道指定的堆棧和容量是否正確。 這是因為遵循ABI的程序不能靜態地決定堆棧的大小。它的大小依賴于運行時的 環境。
2.1.11.2建立你自己的堆棧
如果你指定了線程堆棧的大小,要保證你考慮到了調用它的函數和它調用的函數需要的空間。需要把調用結果、本地變量和消息結構的成分都考慮進來。
偶爾你需要一個與缺省堆棧略有不同的堆棧。一個典型的情況是當線程需 要一兆以上的堆棧空間。一個不太典型的情況是缺省堆棧對于你來說太大了。 你可能會創建上千個線程,如果使用缺省堆棧時,就需要上G的空間。
堆棧的上限是很顯然的,但下限呢?一定要有足夠的堆棧空間來保存堆棧 框架和本地變量。
你可以用thr_min_stack()函數來獲得絕對的最小堆棧容量,它返回運行一 個空過程所需要的堆棧空間。有實際用途的線程需要的更多,所以在減小線程 堆棧的時候要小心。
你通過兩種方式指定一個堆棧。第一種是給堆棧地址賦空值,由實時的運 行庫來為堆棧分配空間,但需要給stack_size參數提供一個期望的值。
另外一種方式是全面了解堆棧管理,為thr_create函數提供一個堆棧的指 針。這意味著你不但要負責為堆棧分配空間,你還要考慮在線程結束后釋放這 些空間。
在你為自己的堆棧分配空間之后,一定要調用一個mprotect(2)函數來為它 附加一個紅區。
Start_routine--指定新線程首先要執行的過程。當start_routine返回時, 線程用該返回值作為退出碼退出(參考thr_exit(3T))。
注意,你只能指定一個參數。如果你想要多參數,把他們作成一個(例如 寫入一個結構)。這個參數可以是任何一個由void說明的數據,典型的是一個 4字節的值。任何更大的值都需要用指針來間接傳送。
Flags--指定創建線程的屬性。在多數情況下提供0即可。
Flags的值通過位或操作來賦。
THR_SUSPENDED--新線程掛起,在thr_continue()后再執行 start_routine。用這種辦法在運行線程之前對它進行操作(例如改變 優先級)。分離線程的終止被忽略。
THR_DETACHED--將新線程分離,使線程一旦終止,其資源可以得到立刻 回收利用。如果你不需要等待線程結束,設置此標志。 如果沒有明確的同步要求,一個不掛起的,分離的線程可以在它 的創建者調用的thr_create函數返回之前終止并將線程號和其他資源 移交給其他線程使用。
THR_BOUND--將一個新線程永久綁定在一個LWP上(新線程為綁定線程)。
THR_NEW_LWP--給非綁定線程的同時性等級加1。效果類似于用 thr_setconcurrency(3T)來增加同時性等級,但是使用 thr_setconcurrency()不影響等級設置。典型的,THR_NEW_LWP在LWP池 內增加一個LWP來運行非綁定線程。
如果你同時指定了THR_BOUND和THR_NEW_LWP,兩個LWP被創建,一 個被綁定在該線程上,另外一個來運行非綁定線程。
THR_DAEMON--標志新線程為守護線程。當所有的非守護線程退出后進程 結束。守護線程不影響進程退出狀態,在統計退出的線程數時被忽略。 一個進程可以通過調用exit(2)或者在所有非守護線程調用 thr_exit(3T)函數終止的時候終止。一個應用程序,或它調用的一個庫, 可以創建一個或多個在決定是否退出的時候被忽略的線程。用 THR_DAEMON標志創建的線程在進程退出的范疇不被考慮。 New_thread--在thr_create()成功返回后,保存指向存放新線程ID的地址。 調用者負責提供保存這個參數值指向的空間。 如果你對這個值不感興趣,給它賦值0。 返回值--thr_thread在正常執行后返回0,其他值意味著錯誤。在以下情況 發生時,函數失敗并返回相關值。
EAGAIN 超過系統限制,例如創建了太多的LWP。
ENOMEM 內存不夠創建新線程。
EINVAL stack_base非空,但stack_size比thr_minstack()的返回值小。
2.1.11.3 Thr_create(3T)例程
例2-5顯示了怎樣用一個與創建者(orig_mask)不同的新的信號掩模來創建新線程。
在這個例子當中,new_mask被設置為屏蔽SIGINT以外的任何信號。然后創建者的信號掩模被改變,以便新線程繼承一個不同的掩模,在thr_create()返回后,創建者的掩模被恢復為原來的樣子。
例子假設SIGINT不被創建者屏蔽。如果最初是屏蔽的,用相應的操作去掉屏蔽。另外一種辦法是用新線程的start routine來設置它自己的信號掩模。
Code Example 2-5 thr_create() Creates Thread With New Signal Mask
thread_t tid;
sigset_t new_mask, orig_mask;
int error;
(void)sigfillset(&new_mask);
(void)sigdelset(&new_mask, SIGINT);
(void)thr_sigsetmask(SIGSETMASK, &new_mask, &orig_mask):
error = thr_create(NULL, 0, dofunc, NULL, 0, &tid);
(void)thr_sigsetmask(SIGSETMASK, NULL, &orig_mask);
2.1.12獲得最小堆棧
thr_min_stack(3T) 用thr_min_stack(3T)來獲得線程的堆棧下限
#include
size_t thr_min_stack(void);
thr_min_stack()返回執行一個空線程所需要的堆棧大小(空線程是一個創 建出來執行一個空過程的線程)。
如果一個線程執行的不僅僅是空過程,應當給它分配比thr_min_stack()返 回值更多的空間。
如果線程創建時由用戶指定了堆棧,用戶應當為該線程保留足夠的空間。在 一個動態連接的環境里,確切知道線程所需要的最小堆棧是非常困難的。
大多數情況下,用戶不應當自己指定堆棧。用戶指定的堆棧僅僅用來支持那 些希望控制它們的執行環境的應用程序。
一般的,用戶應當讓線程庫來處理堆棧的分配。線程庫提供的缺省堆棧足夠 運行任何線程。
2.1.13設置線程的同時性等級
2.1.13.1 thr_getconcurrency(3T)
用thr_getconcurrency()來獲得期望的同時性等級的當前值。實際上同時活動的線程數可能會比這個數多或少。
#include
int thr_getconcurrency(void)
返回值--thr_getconcurrency()為期望的同時性等級返回當前值。
2.1.13.2 Thr_setconcurrency(3T)
用thr_setconcurrency()設置期望的同時性等級。
#include
int thr_setconcurrency(new_level)
進程中的非綁定線程可能需要同時活動。為了保留系統資源,線程系統的缺 省狀態保證有足夠的活動線程來運行一個進程,防止進程因為缺少同時性而死鎖。
因為這也許不會創建最有效的同時性等級,thr_setconcurrency()允許應用 程序用new_level給系統一些提示,來得到需要的同時性等級。
實際的同時活動的線程數可能比new_level多或少。
注意,如果沒有用thr_setconcurrency調整執行資源,有多個 compute-bound(????)線程的應用程序將不能分配所有的可運行線程。
你也可以通過在調用thr_create()時設置THR_NEW_LWP標志來獲得期望的同時性等級。
返回值--thr_setconcurrency()在正常執行后返回0,其他值意味著錯誤。在以下情況發生時,函數失敗并返回相關值。
EAGAIN 指定的同時性等級超出了系統資源的上限。
EINVAL new_level的值為負。
2.1.14得到或設定線程的優先級
一個非綁定線程在調度時,系統僅僅考慮進程內的其他線程的簡單的優先級, 不做調整,也不涉及內核。線程的系統優先級的形式是唯一的,在創建進程時繼 承而來。
2.1.14.1 Thr_getprio(3T)
用thr_getprio()來得到線程當前的優先級。
#include
int thr_getprio(thread_t target_thread,int *pri)
每個線程從它的創建者那里繼承優先級,thr_getprio把target_thread當前 的優先級保存到由pri指向的地址內。
返回值--thr_getprio()在正常執行后返回0,其他值意味著錯誤。在以下情 況發生時,函數失敗并返回相關值。
ESRCH target_thread在當前進程中不存在。
2.1.14.2 Thr_setprio(3T)
用thr_setprio()來改變線程的優先級。
#include
int thr_setprio(thread_t target_thread,int pri)
thr_setprio改變用target_thread指定的線程的優先級為pri。缺省狀態下, 線程的調度是按照固定的優先級--從0到最大的整數--來進行的,即使不全由優先 級決定,它也占有非常重要的地位。Target_thread將打斷低優先級的線程,而讓 位給高優先級的線程。
返回值--thr_setprio()在正常執行后返回0,其他值意味著錯誤。在以下情況發生時,函數失敗并返回相關值。
ESRCH target_thread在當前進程中找不到。
EINVAL pri的值對于和target_thread相關的調度等級來說沒有意義。
2.1.15線程調度和線程庫函數
下面的libthread函數影響線程調度
2.1.15.1 thr_setprio()和thr_getprio()
這兩個函數用來改變和檢索target_thread的優先級,這個優先級在用戶級線程庫調度線程時被引用,但與操作系統調度LWP的優先級無關。
這個優先級影響線程和LWP的結合--如果可運行的線程比LWP多的時候,高優 先級的線程得到LWP。線程的調度是"專橫"的,就是說,如果有一個高優先級的線 程得不到空閑的LWP,而一個低優先級的線程占有一個LWP,則低優先級的線程被 迫將LWP讓給高優先級的線程。
2.1.15.2 thr_suspend()和thr_continue()
這兩個函數控制線程是否被允許運行。調用thr_suspend(),可以把線程設置 為掛起狀態。就是說,該線程被擱置,即使有可用的LWP。在其他線程以該線程為 參數調用thr_continue后,線程退出掛起狀態。這兩個函數應當小心使用--它們 的結果也許是危險的。例如,被掛起的線程也許是處在互鎖狀態的,將它掛起可 能會導致死鎖。
一個線程可以在創建時用THR_SUSPENDED標志設置為掛起。
2.1.15.3 thr_yield()
Thr_yield函數使線程在相同優先級的線程退出掛起狀態后交出LWP。(不會有 更高優先級的線程可運行而沒有運行,因為它會通過強制的方式取得LWP)。這個 函數具有非常重要的意義,因為在LWP上沒有分時的概念(盡管操作系統在執行LWP 時有分時)。
最后,應當注意priocntl(2)也會影響線程調度。更詳細的內容請參照"LWP和調度等級"。
2 用多線程編程
2.1線程(函數)庫(The Threads Library)
用戶級多線程是通過線程庫,libthread來實現的(參考手冊第3頁: library routines)。線程庫支持信號,為可運行的程序排隊,并負責同 時操縱多任務。
這一章討論libthread中的一些通用過程,首先接觸基本操作,然后循 序漸進地進入更復雜的內容。
創建線程-基本特性 Thr_create(3T)
獲得線程號 Thr_self(3T)
執行線程 Thr_yield(3T,the below is same)
掛起或繼續線程 Thr_suspend
Thr_continue
向線程送信號 Thr_kill
設置線程的調用掩模 Thr_sigsetmask
終止線程 Thr-exit
等待線程終止 Thr-join
維護線程的私有數據 Thr_keycreate
Thr_setspecific
Thr_getspecific
創建線程-高級特性 Thr_create
獲得最小堆棧容量 Thr_min_stack
獲得或設置線程的同時性等級 Thr_getconcurrency
Thr_setconcurrency
獲得或設置線程的優先級 Thr_getprio
Thr_setprio
2.1.1創建線程-基本篇
thr_create過程是線程庫所有過程當中最復雜的一個。這部分的內容僅適用于你使用thr_create的缺省參數來創建進程。
對于thr_create更加復雜的使用,包括如何使用自定參數,我們將在高級特性部分給出說明。
thr_create(3T)
這個函數用于在當前進程中添加一個線程。注意,新的線程不繼承未處理的信號,但繼承優先級和信號掩模。
#include
int thr_create(void *stack_base,size_t stack_size,
void *(*start_routine) (void*),void *arg,long flags,
thread_t *new_thread);
size_t thr_min_stack(void);
stack_base--新線程的堆棧地址。如果stack_base是空則thr_create()按 照stack_size為新線程分配一個堆棧。
Stack_size--新線程堆棧的字節數。如果本項為0,將使用缺省值,一般 情況下最好將此項設為0。并不是每個線程都需要指定堆棧空間。線程庫為每個線程的堆棧分配1M 的虛擬內存,不保留交換空間。(線程庫用mmap(2)的MAP_NORESERVE的選項 來實現這種分配)。
Start_routine--指定線程開始執行的函數。如果start_routine返回, 線程將用該函數的返回值作為退出狀態而退出。(參考thr_exit(3T))。
Flags--指定新線程的屬性,一般設置為0。
Flags的值是通過下列內容的位同或來實現的(最后四個flags在高級特性中給出)。
1. THR_DETACHED 將新線程分離,使得它的線程號和其他資源在線程 結束時即可以回收利用。當你不想等待線程終止時,將其置位。如果沒有明確的 同步需求阻礙,一個不掛起的,分離的線程可以在創建者的thr_create返回之前 終止并將其線程號分配給一個心得線程。
2. THR_SUSPENDED掛起新線程,直到被thr_continue喚醒。
3. THR_BOUND把新線程永久綁定在一個LWP上(生成一個綁定線程)。
4. THR_NEW_LWP將非綁定線程的同時性級別加1。
5. THR_DAEMON新線程為一個守護線程。
New_thread--指向存儲新線程ID的地址。多數情況下設置為0。
Return Values--thr_create()在成功執行后返回0并退出。任何其他返回值表明有錯誤發生。當以下情況被檢測到時,thr_create()失敗并返回響應的值。
EAGAIN :超出了系統限制,例如創建了太多的LWP。
ENOMEM:可用內存不夠創建新線程。
EINVAL:stack_base不是NULL而且stack_size比thr_minstack()函數返回的最小堆棧要小。
2.1.2獲取線程號
thr_self(3T) 獲得自身的線程號。
#include
thread_t thr_self(void)
返回值--調用者的線程號。
2.1.3放棄執行
thr_yield(3T)
thr_yield停止執行當前線程,將執行權限讓給有相同或更高優先權的線程。
#include
void thr_yield(void);
2.1.4掛起或繼續執行線程
thr_suspend(3T) 掛起線程。
#include
int thr_suspend(thread_t target_thread);
thr_suspend()立即掛起由target_thread指定的線程。在thr_suspend成功返回后,掛起的線程不再執行。后繼的thr_suspend無效。
Return Values--執行成功后返回0。其他返回值意味著錯誤。以下情況發生時,thr_suspend()失敗并返回相關值。
ESRCH: 在當前進程中找不到target_thread。
Thr_continue(3T)
Thr_continue()恢復執行一個掛起的線程。一旦線程脫離掛起狀態,后繼的
thr_continue將無效。
#include
int thr_continue(thread_t target_thread);
一個掛起的線程不會被信號喚醒。信號被掛起知道線程被thr-continue恢復執行。
返回值--成功執行后返回0。其他值意味著錯誤。在以下情況發生時,函數失敗并返回相關值。
ESRCH:target_thread在當前進程中找不到。
2.1.5向線程發信號
thr_kill(3T)向線程發信號
#include
#include
int thr_kill(thread_t target_thread,int sig);
thr_kill向線程號為target_thread的線程發送信號sig。Target_thread一定要與調用線程處于同一個進程內。參數sig一定是signal(5)中定義過的。
當sig是0時,錯誤檢查將被執行,沒有實際的信號被發送。這可以用來檢測arget_thread參數是否合法。
返回值--成功執行后返回0,其他值意味著錯誤。在以下情況發生時,函數失敗并返回相關值。
EINVAL:sig非法;
ESRCH:target_thread找不到;
2.1.6設置本線程的信號掩模
thr_sigsetmask(3T) 獲取或改變本線程的信號掩模(signal mask)
#include
#include
int thr_sigsetmask(int how,const sigset_t *set,sigset_t *oset);
how參數決定信號設置將被如何改變,可以是下列值之一:
SIG_BLOCK--在當前信號掩模上增加set,set指要阻塞的信號組。
SIG_UNBLOCK--在當前信號掩模上去掉set,set指要解除阻塞的信號組。
SIG_SETMASK--用新的掩模代替現有掩模,set指新的信號掩模。
當set的值是NULL時,how的值并不重要,信號掩模將不被改變。所以,要查詢當前的信號掩模,就給set賦值為NULL。
當參數oset不是NULL時,它指向以前的信號掩模存放的地方。
Return Values--正常執行后返回0。其他值意味著錯誤。在以下情況發生時, 函數失敗并返回相關值。
EINVAL:set不是NULL且how沒有被定義;
EFAULT:set或oset不是合法地址;
2.1.7終止線程
thr_exit(3T)
用來終止一個線程。
#include
void thr_exit(void *status);
thr_exit 函數終止當前線程。所有的私有數據被釋放。如果調用線程不是一個分離線程,線程的ID和返回狀態保留直到有另外的線程在等待。否則返回狀態被忽略,線程號被立刻重新使用。
返回值--當調用線程是進程中的最后一個非守護線程,進程將用狀態0退出。 當最初的線程從main()函數中返回時進程用該線程main函數的返回值退出。
線程可以通過兩種方式停止執行。第一種是從最初的過程中返回。第二種是 提供一個退出代碼,通過調用thr_exit()結束。下面的事情依賴于在線程創建時 flags的設置。
線程A終止的缺省操作(當flags的相應位設為0時,執行缺省操作)是保持 狀態,直到其它線程(不妨設為B)通過"聯合"的方式得知線程A已經死亡。聯合 的結果是B線程得到線程A的退出碼,A自動消亡。你可以通過位或來給flags的 THR_DETACHED參數置位,使得線程在thr_exit()之后或從最初過程返回后立即消 亡。在這種情況下,它的退出碼不會被任何線程獲得。
有一個重要的特殊情況,在主線程--即最初存在的線程--從主函數返回或調 用了exit(),整個進程將終止。所以在主線程中要注意不要過早地從主函數main 返回。
如果主線程僅僅調用了thr_exit(),僅僅是它自己死亡,進程不會結束,進 程內的其他線程將繼續運行(當然,如果所有的線程都結束,進程也就結束了)。
如果一個線程是非分離的,在它結束后一定要有其它進程與它"聯合",否則 該線程的資源就不會被回收而被新線程使用。所以如果你不希望一個線程被 "聯合",最好按照分離線程來創建。
另外一個flag參數是THR_DAEMON。使用這個標志創建的線程是守護線程,在 其他線程終止之后,這些線程自動終止。這些守護線程在線程庫內部特別有用。 守護線程可以用庫內函數創建--在程序的其他部分是不可見的。當程序中所 有的其他線程終止,這些線程自動終止。如果它們不是守護線程,在其它線程終 止后他們不會自動終止,進程不會自動結束。
2.1.8等待線程結束
thr_join(3T) 用thr_join函數來等待線程終止。
#include
int thr_join(thread_t wait_for,thread_t *departed,void **status);
thr_join()函數阻塞自身所在的線程,直到由wait_for指定的線程終止。指 定的線程一定與本線程在同一個進程內部,而且一定不是分離線程。當wait_for 參數為0時,thr_join等待任何一個非分離線程結束。換句話說,當不指定線程 號時,任何非分離線程的退出將導致thr_join()返回。
當departed參數不是NULL時,在thr_join正常返回時它指向存放終止線程ID 的地址。當status參數不是NULL時,在thr_join正常返回時它指向存放終止線程 退出碼的地址。
如果線程創建時指定了堆棧,在thr_join返回時堆棧可以被回收。由它返回 的線程號可以被重新分配。
不能有兩個線程同時等待同一個線程,如果出現這種情況,其中一個線程正 常返回,另外一個返回ESRCH錯誤。
返回值--thr_join()在正常執行后返回0,其他值意味著錯誤。在以下情況 發生時,函數失敗并返回相關值。
ESRCH wait_for不合法,等待的線程為分離現成。
EDEADLK 等待自身結束。
最后步驟
thr_join()有三個參數,提供了一定的靈活性。當你需要一個線程等待 直到另外一個指定的線程結束,應當把后者的ID提供為第一參數。如果 需要等待到任何其他的線程結束,將第一參數置零。
如果調用者想知道是那個線程終止,第二參數應當是儲存死線程的ID的地址。 如果不感興趣,將該參數置零。最后如果需要知道死線程的退出碼,應當指出接 收該錯誤碼的地址。
一個線程可以通過以下的代碼等待所有的非守護線程結束:
while(thr_join(0,0,0)==0)
第三個參數的聲明(void **)看上去很奇怪。相應的thr_exit()的參數為 void *。這樣做的意圖在于你的錯誤代碼為定長的四字節,c語言給定長4字節的 定義不能是void型,因為這以為著沒有參數。所以用void*。因為thr_join()的 第三參數必須是一個指向thr_exit()返回值的指針,所以類型必須是void **。
注意,thr_join()只在目標線程為非分離時有效。如果沒有特殊的同步要求 的話,線程一般都設置成分離的。
可以認為,分離線程是通常意義下的線程,而非分離線程知識特殊情況。
2.1.9簡單的例程
在例子2-1里,一個運行在頂部的線程,創建一個輔助線程來執行fetch過程, 這個輔助過程涉及到復雜的數據庫查詢,需要較長的時間。主線程在等待結果的 時候還有其他事情可做。所以它通過執行thr_join()來等待輔助過程結束。
操作結果被當作堆棧參數傳送,因為主線程等待spun-off線程結束。在一般 意義上,用malloc()存儲數據比通過線程的堆棧來存儲要好一些。????
Code Example 2-1 A Simple Threads Program
Void mainline(){
Char int result;
Thread_t helper;
Int status;
Thr_create(0,0,fetch,&result,0,&helper);
/* do something else for a while */
Thr_join(helper,0,&status);
/* it's now safe to use result*/
}
void fetch(int * result){
/*fetch value from a database */
*result=value;
thr_exit(0);
}
2.1.10維護線程專有數據
單線程C程序有兩種基本數據--本地數據和全局數據。多線程C程序增加了 一個特殊類型--線程專有數據(TSD)。非常類似與全局數據,只不過它是線程 私有的。
TSD是以線程為界限的。TSD是定義線程私有數據的唯一方法。每個線程專有 數據項都由一個進程內唯一的關鍵字(KEY)來標識。用這個關鍵字,線程可以 來存取線程私有的數據。
維護TSD的方法通過以下三個函數進行:
· thr_keycreate()--創建關鍵字
· thr_setspecific()--將一個線程綁定在一個關鍵字上
· thr_getspecific()--存儲指定地址的值
2.1.10.1 thr_keycreate(3T)
thr_keycreate()在進程內部分配一個標識TSD的關鍵字。關鍵字是進程內部唯一的,所有線程在創建時的關鍵字值是NULL。
一旦關鍵字被建立,每一個線程可以為關鍵字綁定一個值。這個值對于綁定的線程來說是唯一的,被每個線程獨立維護。
#include
int thr_keycreate(thread_key_t keyp,
void (*destructor)(void *value);
如果thr_keycreate()成功返回,分配的關鍵字被存儲在由keyp指向的區 域里。調用者一定要保證存儲和對關鍵字的訪問被正確地同步。
一個可選的析構函數,destructor,可以和每個關鍵字聯系起來。如果一 個關鍵字的destructor不空而且線程給該關鍵字一個非空值,在線程退出時該 析構函數被調用,使用當前的綁定值。對于所有關鍵字的析構函數執行的順序 是不能指定的。
返回值--thr_keycreate()在正常執行后返回0,其他值意味著錯誤。在以 下情況發生時,函數失敗并返回相關值。
EAGAIN 關鍵字的名字空間用盡
ENOMEM 內存不夠
2.1.10.2 Thr_setspecific(3T)
#include
int thr_setspecific(thread_key_t key,void *value);
thr_setspecific()為由key指定的TSD關鍵字綁定一個與本線程相關的值。 返回值--thr_setspecific在正常執行后返回0,其他值意味著錯誤。在以 下情況發生時,函數失敗并返回相關值。
ENOMEM 內存不夠
EINVAL 關鍵字非法
2.1.10.3 Thr_getspecific(3T)
#include
int thr_getspecific(thread_key_t key,void **valuep);
thr_getspecific()將與調用線程相關的關鍵字的值存入由valuep指定的區
域。
返回值--thr_getspecific()在正常執行后返回0,其他值意味著錯誤。在 以下情況發生時,函數失敗并返回相關值。
EINVAL 關鍵字非法。
2.1.10.5 全局和私有的線程專有數據
例程2-2是從一個多線程程序中摘錄出來的。這段代碼可以被任意數量的線 程執行,但一定要參考兩個全局變量:errno和mywindow,這兩個值是因線程而 異的,就是說是線程私有的。
Code Example 2-2 線程專有數據--全局且私有的
Body(){
……
while(srite(fd,buffer,size)==-1){
if(errno!=EINTR){
fprintf(mywindow,"%s/n",strerror(errno));
exit(1);
}
}
………
}
本線程的系統錯誤代碼errno可以通過線程的系統調用來獲得,而不是通過 其他線程。所以一個線程獲得的錯誤碼與其他線程是不同的。
變量mywindow指向一個線程私有的輸入輸出流。所以,一個線程的mywindow 和另外一個線程是不同的,因而最終體現在不同的窗口里。唯一的區別在于線程 庫來處理errno,而程序員需要精心設計mywindow。
下面一個例子說明了mywindow的設計方法。處理器把mywindow的指針轉換成為對_mywindow過程的調用。
然后調用thr_getspecific(),把全程變量mywindow_key和標識線程窗口的輸出參數win傳遞給它。
Code Example 2-3 將全局參考轉化為私有參考
#define mywindow _mywindow()
thread_key_t mywindow_key;
FILE * _mywindow(void){
FILE *win;
Thr_getspecific(mywindow_key,&win);
Return(win);
}
void thread_start(…){
…
make_mywindow();
…
}
變量mywindow標識了一類每個線程都有私有副本的變量;就是說,這些變量 是線程專有數據。每個線程調用make_mywindow()來初始化自己的窗口,并且生 成一個指向它的實例mywindow。 一旦過程被調用,現成可以安全地訪問mywindow,在_mywindow函數之后,線 程可以訪問它的私有窗口。所以,對mywindow的操作就象是直接操作線程私有 數據一樣。
Code Example 2-4 顯示了怎樣設置
Code Example 2-4 初始化TSD
Void make_mywindow(void){
FILE **win;
Static int once=0;
Static mutex_t lock;
Mutex_lock(&lock);
If (!once){
Once=1;
Thr_keycreate(&mywindow_key,free_key);
}
mutext_unlock(&lock);
win=malloc(sizeof(*win));
create_window(win,…);
thr_setspecific(mywindow_key,win);
}
void freekey(void *win){
free(win);
}
首先,給關鍵字mywindow_key賦一個唯一的值。這個關鍵字被用于標識 TSD。所以,第一個調用make_mywindow的線程調用thr_keycreate(),這個函 數給其第一個參數賦一個唯一的值。第二個參數是一個析構函數,用來在線程 終止后將TSD所占的空間回收。
下一步操作是給調用者分配一個TSD的實例空間。分配空間以后,調用 create_window過程,為線程建立一個窗口并用win來標識它。最后調用 thr_setspecific(),把win(即指向窗口的存儲區)的值與關鍵字綁在一起。
做完這一步,任何時候線程調用thr_getspecific(),傳送全局關鍵字, 它得到的都是該線程在調用thr_setspecific時與關鍵字綁定的值。 如果線程結束,在thr_keycreate()中建立的析構函數將被調用,每個析構 函數只有在終止的線程用thr_setspecific()為關鍵字賦值之后才會執行。
2.1.11創建線程--高級特性
2.1.11.1 thr_create(3T)
#include
int thr_create(void *stack_base,size_t stack_size,
void *(*start_routine)(void *),void * arg,
long flags,thread_t *newthread);
size_t thr_min_stack(void);
stack_base--新線程所用的堆棧地址。如果本參數為空,thr_create為新線程分配一個至少長stack_size的堆棧。
Stack_size--新線程使用堆棧的字節數。如果本參數為零,將使用缺省值。如果非零,一定要比調用thr_min_stack()獲得的值大。
一個最小堆棧也許不能容納start_routine需要的堆棧大小,所以如果 stack_size被指定,一定要保證它是最小需求與start_routine及它所調用的 函數需要的堆棧空間之和。
典型情況下,由thr_create()分配的線程堆棧從一個頁邊界開始,到離指 定大小最接近的頁邊界結束。在堆棧的頂部放置一個沒有訪問權限的頁,這樣, 大多數堆棧溢出錯誤發生在向越界的線程發送SIGSEGV信號的時候。由調用者分 配的線程堆棧 are used as is . ????
如果調用者使用一個預分配的堆棧,在指向該線程的thr_join()函數返回 之前,堆棧將不被釋放,即使線程已經終止。然后線程用該函數的返回值作為 退出碼退出。
通常情況下,你不需要為線程分配堆棧空間。線程庫為每個線程的堆棧分 配一兆的虛擬內存,不保留交換空間(線程庫用mmap(2)的MAP_NORESERVE選項 來進行分配)。
每個用線程庫創建的線程堆棧有一個"紅區"。線程庫將一個紅區放置在堆 棧頂部來檢測溢出。該頁是沒有訪問權限的,在訪問時將導致一個頁錯誤。紅 區被自動附加在堆棧頂端,不管是用指定的容量還是缺省的容量。
只有在你絕對確信你給的參數正確之后才可以指定堆棧。沒有多少情況需 要去指定堆棧或它的大小。即使是專家也很難知道指定的堆棧和容量是否正確。 這是因為遵循ABI的程序不能靜態地決定堆棧的大小。它的大小依賴于運行時的 環境。
2.1.11.2建立你自己的堆棧
如果你指定了線程堆棧的大小,要保證你考慮到了調用它的函數和它調用的函數需要的空間。需要把調用結果、本地變量和消息結構的成分都考慮進來。
偶爾你需要一個與缺省堆棧略有不同的堆棧。一個典型的情況是當線程需 要一兆以上的堆棧空間。一個不太典型的情況是缺省堆棧對于你來說太大了。 你可能會創建上千個線程,如果使用缺省堆棧時,就需要上G的空間。
堆棧的上限是很顯然的,但下限呢?一定要有足夠的堆棧空間來保存堆棧 框架和本地變量。
你可以用thr_min_stack()函數來獲得絕對的最小堆棧容量,它返回運行一 個空過程所需要的堆棧空間。有實際用途的線程需要的更多,所以在減小線程 堆棧的時候要小心。
你通過兩種方式指定一個堆棧。第一種是給堆棧地址賦空值,由實時的運 行庫來為堆棧分配空間,但需要給stack_size參數提供一個期望的值。
另外一種方式是全面了解堆棧管理,為thr_create函數提供一個堆棧的指 針。這意味著你不但要負責為堆棧分配空間,你還要考慮在線程結束后釋放這 些空間。
在你為自己的堆棧分配空間之后,一定要調用一個mprotect(2)函數來為它 附加一個紅區。
Start_routine--指定新線程首先要執行的過程。當start_routine返回時, 線程用該返回值作為退出碼退出(參考thr_exit(3T))。
注意,你只能指定一個參數。如果你想要多參數,把他們作成一個(例如 寫入一個結構)。這個參數可以是任何一個由void說明的數據,典型的是一個 4字節的值。任何更大的值都需要用指針來間接傳送。
Flags--指定創建線程的屬性。在多數情況下提供0即可。
Flags的值通過位或操作來賦。
THR_SUSPENDED--新線程掛起,在thr_continue()后再執行 start_routine。用這種辦法在運行線程之前對它進行操作(例如改變 優先級)。分離線程的終止被忽略。
THR_DETACHED--將新線程分離,使線程一旦終止,其資源可以得到立刻 回收利用。如果你不需要等待線程結束,設置此標志。 如果沒有明確的同步要求,一個不掛起的,分離的線程可以在它 的創建者調用的thr_create函數返回之前終止并將線程號和其他資源 移交給其他線程使用。
THR_BOUND--將一個新線程永久綁定在一個LWP上(新線程為綁定線程)。
THR_NEW_LWP--給非綁定線程的同時性等級加1。效果類似于用 thr_setconcurrency(3T)來增加同時性等級,但是使用 thr_setconcurrency()不影響等級設置。典型的,THR_NEW_LWP在LWP池 內增加一個LWP來運行非綁定線程。
如果你同時指定了THR_BOUND和THR_NEW_LWP,兩個LWP被創建,一 個被綁定在該線程上,另外一個來運行非綁定線程。
THR_DAEMON--標志新線程為守護線程。當所有的非守護線程退出后進程 結束。守護線程不影響進程退出狀態,在統計退出的線程數時被忽略。 一個進程可以通過調用exit(2)或者在所有非守護線程調用 thr_exit(3T)函數終止的時候終止。一個應用程序,或它調用的一個庫, 可以創建一個或多個在決定是否退出的時候被忽略的線程。用 THR_DAEMON標志創建的線程在進程退出的范疇不被考慮。 New_thread--在thr_create()成功返回后,保存指向存放新線程ID的地址。 調用者負責提供保存這個參數值指向的空間。 如果你對這個值不感興趣,給它賦值0。 返回值--thr_thread在正常執行后返回0,其他值意味著錯誤。在以下情況 發生時,函數失敗并返回相關值。
EAGAIN 超過系統限制,例如創建了太多的LWP。
ENOMEM 內存不夠創建新線程。
EINVAL stack_base非空,但stack_size比thr_minstack()的返回值小。
2.1.11.3 Thr_create(3T)例程
例2-5顯示了怎樣用一個與創建者(orig_mask)不同的新的信號掩模來創建新線程。
在這個例子當中,new_mask被設置為屏蔽SIGINT以外的任何信號。然后創建者的信號掩模被改變,以便新線程繼承一個不同的掩模,在thr_create()返回后,創建者的掩模被恢復為原來的樣子。
例子假設SIGINT不被創建者屏蔽。如果最初是屏蔽的,用相應的操作去掉屏蔽。另外一種辦法是用新線程的start routine來設置它自己的信號掩模。
Code Example 2-5 thr_create() Creates Thread With New Signal Mask
thread_t tid;
sigset_t new_mask, orig_mask;
int error;
(void)sigfillset(&new_mask);
(void)sigdelset(&new_mask, SIGINT);
(void)thr_sigsetmask(SIGSETMASK, &new_mask, &orig_mask):
error = thr_create(NULL, 0, dofunc, NULL, 0, &tid);
(void)thr_sigsetmask(SIGSETMASK, NULL, &orig_mask);
2.1.12獲得最小堆棧
thr_min_stack(3T) 用thr_min_stack(3T)來獲得線程的堆棧下限
#include
size_t thr_min_stack(void);
thr_min_stack()返回執行一個空線程所需要的堆棧大小(空線程是一個創 建出來執行一個空過程的線程)。
如果一個線程執行的不僅僅是空過程,應當給它分配比thr_min_stack()返 回值更多的空間。
如果線程創建時由用戶指定了堆棧,用戶應當為該線程保留足夠的空間。在 一個動態連接的環境里,確切知道線程所需要的最小堆棧是非常困難的。
大多數情況下,用戶不應當自己指定堆棧。用戶指定的堆棧僅僅用來支持那 些希望控制它們的執行環境的應用程序。
一般的,用戶應當讓線程庫來處理堆棧的分配。線程庫提供的缺省堆棧足夠 運行任何線程。
2.1.13設置線程的同時性等級
2.1.13.1 thr_getconcurrency(3T)
用thr_getconcurrency()來獲得期望的同時性等級的當前值。實際上同時活動的線程數可能會比這個數多或少。
#include
int thr_getconcurrency(void)
返回值--thr_getconcurrency()為期望的同時性等級返回當前值。
2.1.13.2 Thr_setconcurrency(3T)
用thr_setconcurrency()設置期望的同時性等級。
#include
int thr_setconcurrency(new_level)
進程中的非綁定線程可能需要同時活動。為了保留系統資源,線程系統的缺 省狀態保證有足夠的活動線程來運行一個進程,防止進程因為缺少同時性而死鎖。
因為這也許不會創建最有效的同時性等級,thr_setconcurrency()允許應用 程序用new_level給系統一些提示,來得到需要的同時性等級。
實際的同時活動的線程數可能比new_level多或少。
注意,如果沒有用thr_setconcurrency調整執行資源,有多個 compute-bound(????)線程的應用程序將不能分配所有的可運行線程。
你也可以通過在調用thr_create()時設置THR_NEW_LWP標志來獲得期望的同時性等級。
返回值--thr_setconcurrency()在正常執行后返回0,其他值意味著錯誤。在以下情況發生時,函數失敗并返回相關值。
EAGAIN 指定的同時性等級超出了系統資源的上限。
EINVAL new_level的值為負。
2.1.14得到或設定線程的優先級
一個非綁定線程在調度時,系統僅僅考慮進程內的其他線程的簡單的優先級, 不做調整,也不涉及內核。線程的系統優先級的形式是唯一的,在創建進程時繼 承而來。
2.1.14.1 Thr_getprio(3T)
用thr_getprio()來得到線程當前的優先級。
#include
int thr_getprio(thread_t target_thread,int *pri)
每個線程從它的創建者那里繼承優先級,thr_getprio把target_thread當前 的優先級保存到由pri指向的地址內。
返回值--thr_getprio()在正常執行后返回0,其他值意味著錯誤。在以下情 況發生時,函數失敗并返回相關值。
ESRCH target_thread在當前進程中不存在。
2.1.14.2 Thr_setprio(3T)
用thr_setprio()來改變線程的優先級。
#include
int thr_setprio(thread_t target_thread,int pri)
thr_setprio改變用target_thread指定的線程的優先級為pri。缺省狀態下, 線程的調度是按照固定的優先級--從0到最大的整數--來進行的,即使不全由優先 級決定,它也占有非常重要的地位。Target_thread將打斷低優先級的線程,而讓 位給高優先級的線程。
返回值--thr_setprio()在正常執行后返回0,其他值意味著錯誤。在以下情況發生時,函數失敗并返回相關值。
ESRCH target_thread在當前進程中找不到。
EINVAL pri的值對于和target_thread相關的調度等級來說沒有意義。
2.1.15線程調度和線程庫函數
下面的libthread函數影響線程調度
2.1.15.1 thr_setprio()和thr_getprio()
這兩個函數用來改變和檢索target_thread的優先級,這個優先級在用戶級線程庫調度線程時被引用,但與操作系統調度LWP的優先級無關。
這個優先級影響線程和LWP的結合--如果可運行的線程比LWP多的時候,高優 先級的線程得到LWP。線程的調度是"專橫"的,就是說,如果有一個高優先級的線 程得不到空閑的LWP,而一個低優先級的線程占有一個LWP,則低優先級的線程被 迫將LWP讓給高優先級的線程。
2.1.15.2 thr_suspend()和thr_continue()
這兩個函數控制線程是否被允許運行。調用thr_suspend(),可以把線程設置 為掛起狀態。就是說,該線程被擱置,即使有可用的LWP。在其他線程以該線程為 參數調用thr_continue后,線程退出掛起狀態。這兩個函數應當小心使用--它們 的結果也許是危險的。例如,被掛起的線程也許是處在互鎖狀態的,將它掛起可 能會導致死鎖。
一個線程可以在創建時用THR_SUSPENDED標志設置為掛起。
2.1.15.3 thr_yield()
Thr_yield函數使線程在相同優先級的線程退出掛起狀態后交出LWP。(不會有 更高優先級的線程可運行而沒有運行,因為它會通過強制的方式取得LWP)。這個 函數具有非常重要的意義,因為在LWP上沒有分時的概念(盡管操作系統在執行LWP 時有分時)。
最后,應當注意priocntl(2)也會影響線程調度。更詳細的內容請參照"LWP和調度等級"。
3.
多線程編程指南3--使用同步對象編程
Wednesday, 29. March 2006, 11:52:42
本文出自:BBS水木清華站 作者:Mccartney (coolcat) (2002-01-29 20:28:07)3 使用同步對象來編程
本章定義了四種可用的同步類型,并且討論實現同步的注意事項。
互斥鎖(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(;

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里最復雜的同步機制。這意味著它不象其他原語那樣細致 ???。一個讀寫鎖通常用在讀操作比寫操作頻繁的時候。