嵌入式Linux中文站給大家介紹三種Linux中的常用多線程同步方式:互斥量,條件變量,信號量。
1 互斥鎖
互斥鎖用來保證一段時間內只有一個線程在執行一段代碼。必要性顯而易見:假設各個線程向同一個文件順序寫入數據,最後得到的結果一定是災難性的。
先看下面一段代碼。這是一個讀/寫程序,它們公用一個緩衝區,並且假定一個緩衝區只能保存一條信息。即緩衝區只有兩個狀態:有信息或沒有信息。
- void reader_function ( void );
- void writer_function ( void );
- char buffer;
- int buffer_has_item=0;
- pthread_mutex_t mutex;
- struct timespec delay;
- void main ( void ){
- pthread_t reader;
- /* 定義延遲時間*/
- delay.tv_sec = 2;
- delay.tv_nec = 0;
- /* 用默認屬性初始化一個互斥鎖對象*/
- pthread_mutex_init (&mutex,NULL);
- pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
- writer_function( );
- }
- void writer_function (void){
- while(1){
- /* 鎖定互斥鎖*/
- pthread_mutex_lock (&mutex);
- if (buffer_has_item==0){
- buffer=make_new_item( );
- buffer_has_item=1;
- }
- /* 打開互斥鎖*/
- pthread_mutex_unlock(&mutex);
- pthread_delay_np(&delay);
- }
- }
- void reader_function(void){
- while(1){
- pthread_mutex_lock(&mutex);
- if(buffer_has_item==1){
- consume_item(buffer);
- buffer_has_item=0;
- }
- pthread_mutex_unlock(&mutex);
- pthread_delay_np(&delay);
- }
- }
void reader_function ( void );
void writer_function ( void );
char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void ){
pthread_t reader;
/* 定義延遲時間*/
delay.tv_sec = 2;
delay.tv_nec = 0;
/* 用默認屬性初始化一個互斥鎖對象*/
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
writer_function( );
}
void writer_function (void){
while(1){
/* 鎖定互斥鎖*/
pthread_mutex_lock (&mutex);
if (buffer_has_item==0){
buffer=make_new_item( );
buffer_has_item=1;
}
/* 打開互斥鎖*/
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
void reader_function(void){
while(1){
pthread_mutex_lock(&mutex);
if(buffer_has_item==1){
consume_item(buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
這裏聲明瞭互斥鎖變量mutex,結構pthread_mutex_t爲不公開的數據類型,其中包含一個系統分配的屬性對象。函數pthread_mutex_init用來生成一個互斥鎖。NULL參數表明使用默認屬性。如果需要聲明特定屬性的互斥鎖,須調用函數pthread_mutexattr_init。函數pthread_mutexattr_setpshared和函數pthread_mutexattr_settype用來設置互斥鎖屬性。前一個函數設置屬性pshared,它有兩個取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用來不同進程中的線程同步,後者用於同步本進程的不同線程。在上面的例子中,使用的是默認屬性PTHREAD_PROCESS_ PRIVATE。後者用來設置互斥鎖類型,可選的類型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它們分別定義了不同的上鎖、解鎖機制,一般情況下,選用最後一個默認屬性。
pthread_mutex_lock聲明開始用互斥鎖上鎖,此後的代碼直至調用pthread_mutex_unlock爲止,均被上鎖,即同一時間只能被一個線程調用執行。當一個線程執行到pthread_mutex_lock處時,如果該鎖此時被另一個線程使用,那此線程被阻塞,即程序將等待到另一個線程釋放此互斥鎖。在上面的例子中,使用了pthread_delay_np函數,讓線程睡眠一段時間,就是爲了防止一個線程始終佔據此函數。
在使用互斥鎖的過程中很有可能會出現死鎖:兩個線程試圖同時佔用兩個資源,並按不同的次序鎖定相應的互斥鎖,例如兩個線程都需要鎖定互斥鎖1和互斥鎖2,a線程先鎖定互斥鎖1,b線程先鎖定互斥鎖2,這時就出現了死鎖。此時可以使用函數pthread_mutex_trylock,它是函數pthread_mutex_lock的非阻塞版本,當它發現死鎖不可避免時,它會返回相應的信息,程序員可以針對死鎖做出相應的處理。另外不同的互斥鎖類型對死鎖的處理不一樣,但最主要的還是要程序員自己在程序設計注意這一點。
2 條件變量
前一節中我們講述瞭如何使用互斥鎖來實現線程間數據的共享和通信,互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定。而條件變量通過允許線程阻塞和等待另一個線程發送信號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變量被用來阻塞一個線程,當條件不滿足時,線程往往解開相應的互斥鎖並等待條件發生變化。一旦其它的某個線程改變了條件變量,它將通知相應的條件變量喚醒一個或多個正被此條件變量阻塞的線程。這些線程將重新鎖定互斥鎖並重新測試條件是否滿足。一般說來,條件變量被用來進行線程間的同步。
條件變量的結構爲pthread_cond_t,函數pthread_cond_init()被用來初始化一個條件變量。它的原型爲:
extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
其中cond是一個指向結構pthread_cond_t的指針,cond_attr是一個指向結構pthread_condattr_t的指針。結構pthread_condattr_t是條件變量的屬性結構,和互斥鎖一樣可以用它來設置條件變量是進程內可用還是進程間可用,默認值是PTHREAD_ PROCESS_PRIVATE,即此條件變量被同一進程內的各個線程使用。注意初始化條件變量只有未被使用時才能重新初始化或被釋放。釋放一個條件變量的函數爲pthread_cond_ destroy(pthread_cond_t cond)。
函數pthread_cond_wait()使線程阻塞在一個條件變量上。它的函數原型爲:
extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex));
線程解開mutex指向的鎖並被條件變量cond阻塞。線程可以被函數pthread_cond_signal和函數pthread_cond_broadcast喚醒,但是要注意的是,條件變量只是起阻塞和喚醒線程的作用,具體的判斷條件還需用戶給出,例如一個變量是否爲0等等,這一點從後面的例子中可以看到。線程被喚醒後,它將重新檢查判斷條件是否滿足,如果還不滿足,一般說來線程應該仍阻塞在這裏,被等待被下一次喚醒。這個過程一般用while語句實現。
另一個用來阻塞線程的函數是pthread_cond_timedwait(),它的原型爲:
extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex, __const struct timespec *__abstime));
它比函數pthread_cond_wait()多了一個時間參數,經歷abstime段時間後,即使條件變量不滿足,阻塞也被解除。
函數pthread_cond_signal()的原型爲:
extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
它用來釋放被阻塞在條件變量cond上的一個線程。多個線程阻塞在此條件變量上時,哪一個線程被喚醒是由線程的調度策略所決定的。要注意的是,必須用保護條件變量的互斥鎖來保護這個函數,否則條件滿足信號有可能在測試條件和調用pthread_cond_wait函數之間被髮出,從而造成無限制的等待。下面是使用函數pthread_cond_wait()和函數pthread_cond_signal()的一個簡單的例子。
- pthread_mutex_t count_lock;
- pthread_cond_t count_nonzero;
- unsigned count;
- decrement_count () {
- pthread_mutex_lock (&count_lock);
- while(count==0)
- pthread_cond_wait( &count_nonzero, &count_lock);
- count=count -1;
- pthread_mutex_unlock (&count_lock);
- }
- increment_count(){
- pthread_mutex_lock(&count_lock);
- if(count==0)
- pthread_cond_signal(&count_nonzero);
- count=count+1;
- pthread_mutex_unlock(&count_lock);
- }
pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count () {
pthread_mutex_lock (&count_lock);
while(count==0)
pthread_cond_wait( &count_nonzero, &count_lock);
count=count -1;
pthread_mutex_unlock (&count_lock);
}
increment_count(){
pthread_mutex_lock(&count_lock);
if(count==0)
pthread_cond_signal(&count_nonzero);
count=count+1;
pthread_mutex_unlock(&count_lock);
}
count值爲0時,decrement函數在pthread_cond_wait處被阻塞,並打開互斥鎖count_lock。此時,當調用到函數increment_count時,pthread_cond_signal()函數改變條件變量,告知decrement_count()停止阻塞。
函數pthread_cond_broadcast(pthread_cond_t *cond)用來喚醒所有被阻塞在條件變量cond上的線程。這些線程被喚醒後將再次競爭相應的互斥鎖,所以必須小心使用這個函數。
3 信號量
信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。當公共資源增加時,調用函數sem_post()增加信號量。只有當信號量值大於0時,才能使用公共資源,使用後,函數sem_wait()減少信號量。函數sem_trywait()和函數pthread_ mutex_trylock()起同樣的作用,它是函數sem_wait()的非阻塞版本。下面逐個介紹和信號量有關的一些函數,它們都在頭文件/usr/include/semaphore.h中定義。
信號量的數據類型爲結構sem_t,它本質上是一個長整型的數。函數sem_init()用來初始化一個信號量。它的原型爲:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem爲指向信號量結構的一個指針;pshared不爲0時此信號量在進程間共享,否則只能爲當前進程的所有線程共享;value給出了信號量的初始值。
函數sem_post( sem_t *sem )用來增加信號量的值。當有線程阻塞在這個信號量上時,調用這個函數會使其中的一個線程不再阻塞,選擇機制同樣是由線程的調度策略決定的。
函數sem_wait( sem_t *sem )被用來阻塞當前線程直到信號量sem的值大於0,解除阻塞後將sem的值減一,表明公共資源經使用後減少。函數sem_trywait ( sem_t *sem )是函數sem_wait()的非阻塞版本,它直接將信號量sem的值減一。
函數sem_destroy(sem_t *sem)用來釋放信號量sem。
下面來看一個使用信號量的例子。在這個例子中,一共有4個線程,其中兩個線程負責從文件讀取數據到公共的緩衝區,另兩個線程從緩衝區讀取數據作不同的處理(加和乘運算)。
- /* File sem.c */
- #include
- #include
- #include
- #define MAXSTACK 100
- int stack[MAXSTACK][2];
- int size=0;
- sem_t sem;
- /* 從文件1.dat讀取數據,每讀一次,信號量加一*/
- void ReadData1(void){
- FILE *fp=fopen(“1.dat”,“r”);
- while(!feof(fp)){
- fscanf(fp,”%d %d”,&stack[size][0],&stack[size][1]);
- sem_post(&sem);
- ++size;
- }
- fclose(fp);
- }
- /*從文件2.dat讀取數據*/
- void ReadData2(void){
- FILE *fp=fopen(“2.dat”,“r”);
- while(!feof(fp)){
- fscanf(fp,”%d %d”,&stack[size][0],&stack[size][1]);
- sem_post(&sem);
- ++size;
- }
- fclose(fp);
- }
- /*阻塞等待緩衝區有數據,讀取數據後,釋放空間,繼續等待*/
- void HandleData1(void){
- while(1){
- sem_wait(&sem);
- printf(”Plus:%d+%d=%d\n”,stack[size][0],stack[size][1],
- stack[size][0]+stack[size][1]);
- –size;
- }
- }
- void HandleData2(void){
- while(1){
- sem_wait(&sem);
- printf(”Multiply:%d*%d=%d\n”,stack[size][0],stack[size][1],
- stack[size][0]*stack[size][1]);
- –size;
- }
- }
- int main(void){
- pthread_t t1,t2,t3,t4;
- sem_init(&sem,0,0);
- pthread_create(&t1,NULL,(void *)HandleData1,NULL);
- pthread_create(&t2,NULL,(void *)HandleData2,NULL);
- pthread_create(&t3,NULL,(void *)ReadData1,NULL);
- pthread_create(&t4,NULL,(void *)ReadData2,NULL);
- /* 防止程序過早退出,讓它在此無限期等待*/
- pthread_join(t1,NULL);
- }
/* File sem.c */
#include
#include
#include
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 從文件1.dat讀取數據,每讀一次,信號量加一*/
void ReadData1(void){
FILE *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*從文件2.dat讀取數據*/
void ReadData2(void){
FILE *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*阻塞等待緩衝區有數據,讀取數據後,釋放空間,繼續等待*/
void HandleData1(void){
while(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}
void HandleData2(void){
while(1){
sem_wait(&sem);
printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}
int main(void){
pthread_t t1,t2,t3,t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* 防止程序過早退出,讓它在此無限期等待*/
pthread_join(t1,NULL);
}
在Linux下,用命令gcc -lpthread sem.c -o sem生成可執行文件sem。事先編輯好數據文件1.dat和2.dat,假設它們的內容分別爲1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,運行sem,得到如下的結果:
- Multiply:-1*-2=2
- Plus:-1+-2=-3
- Multiply:9*10=90
- Plus:-9+-10=-19
- Multiply:-7*-8=56
- Plus:-5+-6=-11
- Multiply:-3*-4=12
- Plus:9+10=19
- Plus:7+8=15
- Plus:5+6=11
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11
從中可以看出各個線程間的競爭關係。而數值並未按原先的順序顯示出來,這是由於size這個數值被各個線程任意修改的緣故。這也往往是多線程編程要注意的問題。