Linux同步機制(二) - 條件變量,信號量,文件鎖,柵欄

1 條件變量

條件變量是一種同步機制,允許線程掛起,直到共享數據上的某些條件得到滿足。

1.1 相關函數

 #include <pthread.h>
 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
 int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t*cond_attr);
 int pthread_cond_signal(pthread_cond_t *cond);
 int pthread_cond_broadcast(pthread_cond_t *cond);
 int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
 int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t*mutex, const struct timespec *abstime);
 int pthread_cond_destroy(pthread_cond_t *cond);

1.2 說明

1. 條件變量是一種同步機制,允許線程掛起,直到共享數據上的某些條件得到滿足。條件變量上的基本操作有:觸發條件(當條件變爲true);等待條件,掛起線程直到其他線程觸發條件。
2. 條件變量要和互斥量相聯結,以避免出現條件競爭--一個線程預備等待一個條件變量,當它在真正進入等待之前,另一個線程恰好觸發了該條件。
pthread_cond_init 使用 cond_attr指定的屬性初始化條件變量 cond,當 cond_attr NULL 時,使用缺省的屬性。LinuxThreads實現條件變量不支持屬性,因此 cond_attr參數實際被忽略。
pthread_cond_t 類型的變量也可以用 PTHREAD_COND_INITIALIZER常量進行靜態初始化。
pthread_cond_signal 使在條件變量上等待的線程中的一個線程重新開始。如果沒有等待的線程,則什麼也不做。如果有多個線程在等待該條件,只有一個能重啓動,但不能指定哪一個。
pthread_cond_broadcast 重啓動等待該條件變量的所有線程。如果沒有等待的線程,則什麼也不做。
pthread_cond_wait 自動解鎖互斥量(如同執行了 pthread_unlock_mutex),並等待條件變量觸發。這時線程掛起,不佔用 CPU 時間,直到條件變量被觸發。在調用 pthread_cond_wait 之前,應用程序必須加鎖互斥量。pthread_cond_wait 函數返回前,自動重新對互斥量加鎖(如同執行了 pthread_lock_mutex)
互斥量的解鎖和在條件變量上掛起都是自動進行的。因此,在條件變量被觸發前,如果所有的線程都要對互斥量加鎖,這種機制可保證在線程加鎖互斥量和進入等待條件變量期間,條件變量不被觸發。
pthread_cond_timedwait pthread_cond_wait一樣,自動解鎖互斥量及等待條件變量,但它還限定了等待時間。如果在 abstime指定的時間內 cond 未觸發,互斥量 mutex被重新加鎖,且 pthread_cond_timedwait返回錯誤 ETIMEDOUTabstime參數指定一個絕對時間,時間原點與 time gettimeofday相同:abstime = 0表示 1970 1 1 00:00:00 GMT
pthread_cond_destroy 銷燬一個條件變量,釋放它擁有的資源。進入 pthread_cond_destroy之前,必須沒有在該條件變量上等待的線程。在 LinuxThreads的實現中,條件變量不聯結資源,除檢查有沒有等待的線程外,pthread_cond_destroy實際上什麼也不做。
3. 取消                                                                                         
pthread_cond_wait
pthread_cond_timedwait是取消點。如果一個線程在這些函數上掛起時被取消,線程立即繼續執行,然後再次對 pthread_cond_wait pthread_cond_timedwait mutex參數加鎖,最後執行取消。因此,當調用清除處理程序時,可確保,mutex是加鎖的。
4. 異步信號安全(Async-signalSafety)                                                                                         
條件變量函數不是異步信號安全的,不應當在信號處理程序中進行調用。特別要注意,如果在信號處理程序中調用 pthread_cond_signal pthread_cond_boardcast函數,可能導致調用線程死鎖。
5. 返回值

在執行成功時,所有條件變量函數都返回 0,錯誤時返回非零的錯誤代碼。
6. 錯誤代碼
pthread_cond_init,   pthread_cond_signal, pthread_cond_broadcast, pthread_cond_wait從不返回錯誤代碼。
pthread_cond_timedwait 函數出錯時返回下列錯誤代碼:
ETIMEDOUT   abstime 指定的時間超時時,條件變量未觸發
EINTR       pthread_cond_timedwait被觸發中斷
pthread_cond_destroy 函數出錯時返回下列錯誤代碼:
EBUSY       某些線程正在等待該條件變量
7. 舉例                                                                                    
設有兩個共享的變量 x y,通過互斥量 mut保護,當 x > y時,條件變量 cond被觸發。
int x,y;
int x,y;
pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
                                                                                         
等待直到 x > y 的執行流程:
pthread_mutex_lock(&mut);
while (x <= y) {
    pthread_cond_wait(&cond, &mut);
}
/*
xy進行操作 */
pthread_mutex_unlock(&mut);
                                                                                         
x y的修改可能導致 x > y,應當觸發條件變量:                                                                                         
pthread_mutex_lock(&mut);
/*
修改 xy */
if (x > y) pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mut);
                                                                                         
如果能夠確定最多隻有一個等待線程需要被喚醒(例如,如果只有兩個線程通過 xy通信),則使用 pthread_cond_signal pthread_cond_broadcast 效率稍高一些。如果不能確定,應當用pthread_cond_broadcast

要等待在 5 秒內 x > y,這樣處理:
struct timeval now;
struct timespec timeout;
int retcode;
                                                                                         
pthread_mutex_lock(&mut);
gettimeofday(&now);
timeout.tv_sec = now.tv_sec + 5;
timeout.tv_nsec = now.tv_usec * 1000;
retcode = 0;
while (x <= y && retcode != ETIMEDOUT) {
    retcode =pthread_cond_timedwait(&cond, &mut, &timeout);
}
if (retcode == ETIMEDOUT) {
    /*
發生超時 */
} else {
    /*
操作 x   y */
}
pthread_mutex_unlock(&mut);

============================

2 信號量

Semaphore,即信號量(sem_init),用來保護多重資源的訪問,它可以設置一個大於0的值,如N,任何訪問者在該信號量大於1的情況下均可以獲得資源的訪問權,並將相應的信號量減1。一般在爲了線程在某一定程度上的順序執行才使用信號量,即線程A等待線程B執行完某些操作以後,才能繼續往下執行,可以理解爲,組裝廠A需要等待(sem_wait)元件廠B交付元件以後(sem_post)才能繼續生產。當信號量不再使用時,銷燬它(sem_destroy)

2.1 相關函數

sem_init(sem_t *sem, int pshared, unsignedint value):初始化一個信號量

sem_wait(sem_t *sem):一直等待信號量,直到信號量大於0

int sem_trywait(sem_t *sem):等待信號量,沒有成功立即返回

sem_timedwait(sem_t *sem, const structtimespec *abs_timeout):設定超時等待。

sem_post(sem_t *sem):信號量加1

sem_destory(sem_t *sem):釋放信號量。

 

2.2 代碼講解:信號量的使用

#include <stdio.h> 

#include <stdlib.h> 

#include <unistd.h>

#include <pthread.h> 

#include <semaphore.h> 

 

sem_t binSem;

 

void* helloWorld(void* arg) 

{

    while(1) 

    { 

       // Wait semaphore

       sem_wait(&binSem);

       printf("Hello World\n");

    }

}

 

int main(int argc, char** argv)
{

    // Result for System call

    int res = 0;

 

    // Initialize semaphore

    sem_init(&binSem, 0, 0);

 

    // Create thread

    pthread_t thdHelloWorld;

    pthread_create(&thdHelloWorld, NULL, helloWorld, NULL);

 

    while(1) 

    {

       // Post semaphore

       sem_post(&binSem);

       printf("In main, sleep several seconds.\n");

       sleep(1);

    }

 

   // Wait for thread synchronization

   void *threadResult;

   pthread_join(thdHelloWorld, &threadResult);

 

   return 0;

}

結果說明:

[root@rocket lock-free]# ./pthread_sem

In main, sleep several seconds.

Hello World

In main, sleep several seconds.

Hello World

In main, sleep several seconds.

Hello World

In main, sleep several seconds.

Hello World

In main, sleep several seconds.

Hello World

 

3 文件鎖

3.1 文件鎖介紹

linux下可以使用flock函數對文件進行加鎖解鎖等操作。簡單介紹下flock()函數:

表頭文件#include <sys/file.h>

定義函數 int flock(int fd,int operation);

函數說明 flock()會依參數operation所指定的方式對參數fd所指的文件做各種鎖定或解除鎖定的動作。此函數只能鎖定整個文件,無法鎖定文件的某一區域。

參數 operation有下列四種情況:

LOCK_SH 建立共享鎖定。多個進程可同時對同一個文件作共享鎖定。

LOCK_EX 建立互斥鎖定。一個文件同時只有一個互斥鎖定。

LOCK_UN 解除文件鎖定狀態。

LOCK_NB 無法建立鎖定時,此操作可不被阻斷,馬上返回進程。通常與LOCK_SHLOCK_EXOR(|)組合。

單一文件無法同時建立共享鎖定和互斥鎖定,而當使用dup()fork()時文件描述詞不會繼承此種鎖定。

返回值返回0表示成功,若有錯誤則返回-1,錯誤代碼存於errno

 

3.2 代碼講解:文件鎖的使用

file_lock_a.cpp

#include <stdlib.h>

#include <stdio.h>

#include <unistd.h>

#include <sys/file.h>

 

int main(int argc, char** argv)

{

   FILE *fp = NULL;

   int i = 20; 

   if ((fp = fopen("./file_lock.test", "r+b")) == NULL)//打開文件

   {

       printf("file open error!\n");

       exit(0);

   }

   if (flock(fp->_fileno, LOCK_EX) != 0) //給該文件加鎖

       printf("file lock by others\n");

   while(1) //進入循環,加鎖時間爲20秒,打印倒計時

   {   

       printf("in a, %d\n", i--);

       sleep(1);

       if (i == 0)

           break;

   }   

   fclose(fp); //20秒後退出,關閉文件

   flock(fp->_fileno, LOCK_UN); //文件解鎖

   return 0;

}

file_lock_b.cpp

#include <stdlib.h>

#include <stdio.h>

#include <unistd.h>

#include <sys/file.h>

 

int main(int argc, char** argv)

{

   FILE *fp = NULL;

   int i = 20; 

   if ((fp = fopen("./file_lock.test", "r+b")) == NULL)

   {

       printf("file open error!\n");

       exit(0);

   }

   flock(fp->_fileno, LOCK_EX);

   while(1) //進入循環,加鎖時間爲20秒,打印倒計時

   {   

       printf("in b, %d\n", i--);

       sleep(1);

       if(i == 0)

           break;

   }   

   fclose(fp); //20秒後退出,關閉文件

   flock(fp->_fileno, LOCK_UN); //文件解鎖

   return 0;

}

結果說明:

先創建文件touch file_lock.test

先運行file_lock_a20秒以內在另一個終端運行file_lock_b,可以看到file_lock_a打印結束了file_lock_b纔開始打印的現象。

 

4 柵欄

4.1 相關函數

pthread_barrier 系列函數在<pthread.h>中定義,用於多線程的同步,它包含三個函數:

#include <pthread.h>

int pthread_barrier_init(pthread_barrier_t*restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned count);

int pthread_barrier_wait(pthread_barrier_t*barrier);

intpthread_barrier_destroy(pthread_barrier_t *barrier);

 

參數解釋:

pthread_barrier_t,是一個計數鎖,對該鎖的操作都包含在三個函數內部,我們不用關心也無法直接操作。只需要實例化一個對象丟給它就好。

pthread_barrierattr_t,鎖的屬性設置,設爲NULL讓函數使用默認屬性即可。

count,你要指定的等待個數。

 

4.2 功能說明

那麼pthread_barrier_*是用來做什麼的?這三個函數又怎麼配合使用呢?

pthread_barrier_*其實只做且只能做一件事,就是充當欄杆(barrier意爲欄杆)。形象的說就是把先後到達的多個線程擋在同一欄杆前,直到所有線程到齊,然後撤下欄杆同時放行。

1init函數負責指定要等待的線程個數;

2wait()函數由每個線程主動調用,它告訴欄杆我到起跑線前了wait()執行末尾欄杆會檢查是否所有人都到欄杆前了,如果是,欄杆就消失所有線程繼續執行下一句代碼;如果不是,則所有已到wait()的線程停在該函數不動,剩下沒執行到wait()的線程繼續執行;

3destroy函數釋放init申請的資源。

4.3 使用場景舉例

這種欄杆機制最大的特點就是最後一個執行wait的動作最爲重要,就像賽跑時的起跑槍一樣,它來之前所有人都必須等着。所以實際使用中,pthread_barrier_*常常用來讓所有線程等待起跑槍響起後再一起行動。比如我們可以用pthread_create()生成100個線程,每個子線程在被create出的瞬間就會自顧自的立刻進入回調函數運行。但我們可能不希望它們這樣做,因爲這時主進程還沒準備好,和它們一起配合的其它線程還沒準備好,我們希望它們在回調函數中申請完線程空間、初始化後停下來,一起等待主進程釋放一個開始信號,然後所有線程再開始執行業務邏輯代碼。

 

解決方案:

爲了解決上述場景問題,我們可以在init時指定n+1個等待,其中n是線程數。而在每個線程執行函數的首部調用wait()。這樣100pthread_create()結束後所有線程都停下來等待最後一個wait()函數被調用。這個wait()由主進程在它覺得合適的時候調用就好。最後這個wait()就是鳴響的起跑槍。

 

4.4 代碼講解:barrier的使用

#include <stdio.h>

#include <pthread.h>

#include <unistd.h>

#include <stdlib.h>

 

pthread_barrier_t barrier;

int thread_num = 10;

 

void* doSomething(void* arg)

{

         printf("beforewait %d\n", pthread_self());

         pthread_barrier_wait(&barrier);//所有線程都被阻塞在這裏

         printf("I'min pthread %d\n", pthread_self());

         

         returnNULL;

}

 

int activate()

{ 

         //等一切都安排好了調用該函數。起跑槍“砰!”

         pthread_barrier_wait(&barrier);

         return0;

}

 

int main(int argc, char** argv)

{

         pthread_attr_tattr;

         pthread_attr_init(&attr);

         

         pthread_t*thread = (pthread_t*)malloc(thread_num * sizeof(pthread_t));

         pthread_barrier_init(&barrier,NULL, thread_num + 1);

         for(inti = 0; i < thread_num; i++)

         {

                   pthread_create(thread+i,&attr, doSomething, NULL);

         }

         

         activate();

         

         for(int i = 0; i < thread_num; i++)

         {

                   pthread_join(*(thread+i),NULL);

         }

 

         pthread_attr_destroy(&attr);

         

         return0;

}

代碼說明:

無柵欄時候的運行結果(註釋掉doSomethingactivate中的pthread_barrier_wait)

before wait 169158400

I'm in pthread 169158400

before wait 158668544

I'm in pthread 158668544

before wait 190138112

I'm in pthread 190138112

before wait 179648256

I'm in pthread 179648256

before wait 148178688

I'm in pthread 148178688

before wait 137688832

I'm in pthread 137688832

before wait 127198976

I'm in pthread 127198976

before wait 106219264

I'm in pthread 106219264

before wait 116709120

I'm in pthread 116709120

before wait 95729408

I'm in pthread 95729408

 

有柵欄時候的運行結果

[root@rocket lock-free]# ./pthread_barrier

before wait 2137720576

before wait 2106251008

before wait 2127230720

before wait 2085271296

before wait 2095761152

before wait 2053801728

before wait 2074781440

before wait 2116740864

before wait 2043311872

before wait 2064291584

I'm in pthread 2127230720

I'm in pthread 2137720576

I'm in pthread 2095761152

I'm in pthread 2106251008

I'm in pthread 2085271296

I'm in pthread 2064291584

I'm in pthread 2053801728

I'm in pthread 2074781440

I'm in pthread 2043311872

I'm in pthread 2116740864

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章