默認情況下互斥鎖和條件變量用於線程間同步,若將它們放在共享內存區,也能用於進程間同步。
1、概述:
互斥鎖(Mutex,也稱互斥量),防止多個線程對一個公共資源做讀寫操作的機制,以保證共享數據的完整性。
用以保護臨界區,以保證任何時候只有一個線程(或進程)在訪問共享資源(如代碼段)。保護臨界區的代碼形式:
lock_the_mutex(...);
臨界區
unlock_the_mutex(...);
任何時刻只有一個線程能夠鎖住一個給定的互斥鎖。
下面的三個函數給一個互斥鎖進行上鎖和解鎖:
#include
int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_trylock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mptr);
以上三個函數,如果調用成功均返回0,失敗返回相應的error值。
如果嘗試給一個已由某個線程鎖住的互斥鎖上鎖,那麼pthread_mutex_lock將阻塞到該互斥鎖解鎖為止。pthread_mutex_trylock是對應的非阻塞函數,如果該互斥鎖已鎖住,它就返回一個EBUSY錯誤。
如果有多個線程阻塞在等待同一個互斥鎖上,那麼當該互斥鎖解鎖時,哪一個線程會開始運行:不同線程被賦予不同優先級,同步函數將喚醒優先級最高的被阻塞線程。
2、互斥鎖實現的生產者-消費者模型:
生產者-消費者問題也稱有界緩沖區問題,若干個生產者和若干個消費者共享使用固定數目的緩沖區,因而帶來的同步和通信問題。
各種IPC手段本身就是一個生產者-消費者問題的實例。
管道、FIFO和消息隊列的同步是隱式同步,使用者只能通過指定的接口來使用這些IPC方式,其中的同步都由內核完成。
共享內存作為IPC,需要使用者進行顯式同步,線程間共享全局數據也需要顯式同步。
<喎?http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxjb2RlIGNsYXNzPQ=="hljs r">以多生產者,單消費者的模型為例:
在單個進程中有多個生產者線程和單個消費者線程,我們只關心多個生產者線程之間的同步,直到所有生產者線程都完成工作後,才啟動消費者線程。
#include
#include
#include
#include
#include
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
int nitems; //(1)生產者存放的條目數,只讀(對於生產者或消費者)!!!
/*
*shared結構中的變量是共享數據
*/
struct {//(2)!!!
pthread_mutex_t mutex;/*同步變量:互斥鎖*/
int buff[MAXNITEMS];//生產者會依次給buff數組存放數據
int nput;/*nput是buff數組中下一次存放的元素下標*/
int nval;/*nval是下一次存放的值(0,1,2等)*/
} shared = {
PTHREAD_MUTEX_INITIALIZER //(3)對用於生產者線程間同步的互斥鎖做初始化!!!
};
void *produce(void *), *consume(void *);
int main(int argc,char *argv[])
{
/*
*變量說明:tid_produce[]數組中保存每個線程的線程ID
*count[]是每個線程計數器
*tid_consume中保存單個的消費者的ID
*/
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
/*命令行參數個數判斷*/
if (argc != 3) {
printf("usage: producer_consumer1 <#iterms> <#threads>\n");
exit(1);
}
/*
* argv[1]中指定生產者存放的條目數
* argv[2]中指定待創建的生產者線程的數目
*/
nitems = min(atoi(argv[1]), MAXNITEMS);//(4)指定生產者存放的條目數!!!
nthreads = min(atoi(argv[2]), MAXNTHREADS);//(5)創建多少個生產者線程!!!
/*
*set_concurrency函數用來告訴線程系統我們希望並發運行多少線程
*即設置並發級別
*/
set_concurrency(nthreads);//(6)!!!
//(7)創建生產者線程:每個線程執行produce!!!
for (i = 0; i < nthreads; i++) {//依次將buff[i]設置為i
count[i] = 0;//計數器初始化為0,每個線程每次往緩沖區存放一個條目時給這個計數器加1.
pthread_create(&tid_produce[i], NULL, produce, &count[i]);
}
//(8)等待所有生產者線程終止,並輸出每個線程的計數器值!!!
for (i = 0; i < nthreads; i++) {
pthread_join(tid_produce[i], NULL);
printf("count[%d] = %d\n", i, count[i]);
}
//(9)然後啟動單個消費者線程!!!
pthread_create(&tid_consume, NULL, consume, NULL);
//(10)接著等待消費者完成,然後終止進程!!!
pthread_join(tid_consume, NULL);
return 0;
}
//創建生產者線程
void *produce(void *arg)
{
for ( ; ; ) {
pthread_mutex_lock(&shared.mutex);//(1)上鎖!!!
//(2)臨界區
if (shared.nput >= nitems) {
//說明此時已經生產完畢,解鎖
pthread_mutex_unlock(&shared.mutex);
return (NULL);
}
shared.buff[shared.nput] = shared.nval;
shared.nput++;
shared.nval++;
pthread_mutex_unlock(&shared.mutex);//(3)解鎖!!!
//count元素的增加(通過指針arg)不屬於臨界區,因為每個線程有各自的計數器
*((int *) arg) += 1;
}
}
//等待生產者線程,然後啟動消費者線程
void *consume(void *arg)
{
int i;
/*
*消費者只是驗證buff中的條目是否正確,如果發現錯誤則輸出一條信息
*這個函數是只有一個實例在運行,而且是在所有的生產者線程都完成之後
*因此不需要任何同步
*/
for (i = 0; i < nitems; i++)
if (shared.buff[i] != i)
printf("buff[%d] = %d\n", i, shared.buff[i]);
return (NULL);
}
3、互斥鎖的非正常終止:
若進程在持有互斥鎖時終止,內核不會負責自動釋放持有的鎖。內核自動清理的唯一同步鎖類型是fcntl記錄鎖。
若被鎖住的互斥鎖的持有進程或線程終止,會造成這個互斥鎖無法解鎖,因而死鎖。線程可以安裝線程清理程序,用來在被取消時能釋放持有的鎖。但這種釋放可能會導致共享對象的狀態被部分更新,造成不一致。
1.2 條件變量
互斥鎖只能用於上鎖,實現對某個共享對象的互斥訪問,無法用於對某事件的等待。條件變量則用於等待。
#include
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int int pthread_cond_signal(pthread_cond_t *cond);
每個條件變量都需要關聯一個互斥鎖,用來提供對等待條件的互斥訪問。
2、讀寫鎖
讀寫鎖可以在讀數據與修改數據之間作區分。其規則如下:
1)沒有線程持有寫鎖時,任意多的線程可以持有讀鎖。
2)僅當沒有線程持有讀鎖或寫鎖時,才能分配寫鎖。
簡言之,只要沒有線程在寫,那麼所有線程都可以讀;但是有線程要想寫,必須是既沒有線程在讀,也沒有線程在寫。!!!
當已有線程持有讀鎖時,另一線程申請寫鎖則會阻塞,若後續還有讀鎖的申請,此時有兩種策略:
1)對後續的讀鎖請求都通過,可能會造成因讀鎖不斷被分配,寫鎖申請始終阻塞,“餓死”了寫進程。
2)後續讀鎖請求都阻塞,等當前持有的讀鎖都結束後優先分配寫鎖。
與普通互斥鎖相比,當被保護數據的讀訪問比寫訪問更為頻繁時,讀寫鎖能提供更高的並發度。
3、記錄上鎖
記錄上鎖是讀寫鎖的一種擴展類型,它可用於有親緣關系或無親緣關系的進城之間共享某個文件的讀與寫。
執行上鎖的函數是fcntl,鎖由內核維護,其屬主由進程ID標識。
特點:只用於不同進程間的上鎖,而不是同一進程內不同線程間的上鎖。
Unix內核沒有記錄這一概念,對記錄的解釋是由讀寫文件的應用進行的。每個記錄就是文件中的一個字節范圍。
使用fcntl記錄上鎖時,等待著的讀出者優先還是等待著的寫入者優先沒有保證。
4、信號量
信號量是一種用於不同進程間,或一個給定進程內不同線程間同步手段的原語。
3中信號量類型:
1)Posix有名信號量:使用Posix IPC名字標識,可用於進程或線程間的同步。(可用於彼此無親緣關系的進程間)
2)Posix基於內存的信號量(無名信號量):存放在共享內存區,可用於進程或線程間的同步。(不可用於彼此無親緣關系的進程間)
3)System V信號量:在內核中維護,可用於進程或線程間的同步。
Posix信號量不必在內核中維護(System V信號量由內核維護),由可能為路徑名的名字來標識。
4.1 Posix信號量
1、概述
三種基本操作:
1)創建(create):指定初始值。
2)等待(wait):如果值小於等於0則阻塞,否則將其減一,又稱P操作。
3)掛出(post):將信號量的值加1,加後如果值大於0,則喚醒一個阻塞在等待上的線程,又稱V操作。
信號量的wait和post與條件變量的wait和signal類似,區別是:因為永久的改變了信號量的值,信號量的操作總被記住(會影響到後續的操作);條件變量的signal如果沒有線程在等待,該信號將丟失(對後續操作沒有影響)。
互斥鎖是為上鎖而優化的,條件變量是為等待優化的,信號量既可以上鎖也可以等待,因此開銷更大。
2、二值信號量
二值信號量,其值為0或1,資源鎖住則信號量值為0,若資源可用則信號量值為1。
二值信號量可用於互斥,就像互斥鎖一樣。但互斥鎖必須由鎖住它的線程解鎖,信號量的掛出卻不必由執行過它的等待操作的同一線程執行。
二值信號量用於生產者消費者問題:考慮往某個緩沖區放一個條目的一個生產者,以及取走該條目的一個消費者,這種簡化類型。
3、計數信號量
其值在0和某個限制值(32767內)之間,可統計資源數,信號量的值就是可用資源數。等待操作都等待信號量的值變為大於0(表示可用),然後將它減1;掛出操作則只是將信號量值加1(可用資源數增加),喚醒正在等待該信號量值變為大於0的任意線程。
4.2 System V信號量
System V信號量增加了另一級復雜度。
**計數信號量集:一個或多個信號量(構成一個集合),其中每個都是計數信號量。**System V信號量一般指計數信號量集。而Posix信號量一般指單個計數信號量。
5、信號量、互斥鎖和條件變量的差異
信號量的意圖在於進程間同步,這些進程可能共享也可能不共享內存區;互斥鎖和條件變量的意圖在於線程間同步,這些線程總是共享內存區;但是信號量也可用於線程間,互斥鎖和條件變量也可用於進程間。
1)互斥鎖總是由給他上鎖的線程解鎖,信號量的掛出也可由其他線程執行。
2)互斥鎖要麼被鎖住,要麼被解開(二值狀態,類似於二值信號量)。
3)信號量有一個與之關聯的狀態(計數值),信號量的掛出操作總是被記住。然而當向一個條件變量發信號時,如果沒有線程在等待,信號將丟失。