Linux下執行緒同步的幾種方法
Linux下提供了多種方式來處理執行緒同步,最常用的是互斥鎖、條件變數和訊號量。
一、互斥鎖(mutex)
鎖機制是同一時刻只允許一個執行緒執行一個關鍵部分的程式碼。
1. 初始化鎖
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
其中引數 mutexattr 用於指定鎖的屬性(見下),如果為NULL則使用預設屬性。
互斥鎖的屬性在建立鎖的時候指定,在LinuxThreads實現中僅有一個鎖型別屬性,不同的鎖型別在試圖對一個已經被鎖定的互斥鎖加鎖時表現不同。當前有四個值可供選擇:
(1)PTHREAD_MUTEX_TIMED_NP,這是預設值,也就是普通鎖。當一個執行緒加鎖以後,其餘請求鎖的執行緒將形成一個等待佇列,並在解鎖後按優先順序獲得鎖。這種鎖策略保證了資源分配的公平性。
(2)PTHREAD_MUTEX_RECURSIVE_NP,巢狀鎖,允許同一個執行緒對同一個鎖成功獲得多次,並通過多次unlock解鎖。如果是不同執行緒請求,則在加鎖執行緒解鎖時重新競爭。
(3)PTHREAD_MUTEX_ERRORCHECK_NP,檢錯鎖,如果同一個執行緒請求同一個鎖,則返回EDEADLK,否則與PTHREAD_MUTEX_TIMED_NP型別動作相同。這樣就保證當不允許多次加鎖時不會出現最簡單情況下的死鎖。
(4)PTHREAD_MUTEX_ADAPTIVE_NP,適應鎖,動作最簡單的鎖型別,僅等待解鎖後重新競爭。
2. 阻塞加鎖
int pthread_mutex_lock(pthread_mutex *mutex);
3. 非阻塞加鎖
int pthread_mutex_trylock( pthread_mutex_t *mutex);
該函式語義與 pthread_mutex_lock() 類似,不同的是在鎖已經被佔據時返回 EBUSY 而不是掛起等待。
4. 解鎖(要求鎖是lock狀態,並且由加鎖執行緒解鎖)
int pthread_mutex_unlock(pthread_mutex *mutex);
5. 銷燬鎖(此時鎖必需unlock狀態,否則返回EBUSY)
int pthread_mutex_destroy(pthread_mutex *mutex);
示例程式碼:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int gn; void* thread(void *arg) { printf("thread's ID is %d\n",pthread_self()); pthread_mutex_lock(&mutex); gn = 12; printf("Now gn = %d\n",gn); pthread_mutex_unlock(&mutex); return NULL; } int main() { pthread_t id; printf("main thread's ID is %d\n",pthread_self()); gn = 3; printf("In main func, gn = %d\n",gn); if (!pthread_create(&id, NULL, thread, NULL)) { printf("Create thread success!\n"); }else { printf("Create thread failed!\n"); } pthread_join(id, NULL); pthread_mutex_destroy(&mutex); return 0; }
二、條件變數(cond)
條件變數是利用執行緒間共享全域性變數進行同步的一種機制。條件變數上的基本操作有:觸發條件(當條件變為 true 時);等待條件,掛起執行緒直到其他執行緒觸發條件。
1. 初始化條件變數
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
儘管POSIX標準中為條件變數定義了屬性,但在Linux中沒有實現,因此cond_attr值通常為NULL,且被忽略。
2. 有兩個等待函式
(1)無條件等待
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
(2)計時等待
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
如果在給定時刻前條件沒有滿足,則返回ETIMEOUT,結束等待,其中abstime以與time()系統呼叫相同意義的絕對時間形式出現,0表示格林尼治時間1970年1月1日0時0分0秒。
無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個執行緒同時請求(用 pthread_cond_wait() 或 pthread_cond_timedwait() 請求)競爭條件(Race Condition)。mutex互斥鎖必須是普通鎖(PTHREAD_MUTEX_TIMED_NP)或者適應鎖(PTHREAD_MUTEX_ADAPTIVE_NP),且在呼叫pthread_cond_wait()前必須由本執行緒加鎖(pthread_mutex_lock()),而在更新條件等待佇列以前,mutex保持鎖定狀態,並在執行緒掛起進入等待前解鎖。在條件滿足從而離開pthread_cond_wait()之前,mutex將被重新加鎖,以與進入pthread_cond_wait()前的加鎖動作對應。
3. 激發條件
(1)啟用一個等待該條件的執行緒(存在多個等待執行緒時按入隊順序啟用其中一個)
int pthread_cond_signal(pthread_cond_t *cond);
(2)啟用所有等待執行緒
int pthread_cond_broadcast(pthread_cond_t *cond);
4. 銷燬條件變數
int pthread_cond_destroy(pthread_cond_t *cond);
只有在沒有執行緒在該條件變數上等待的時候才能銷燬這個條件變數,否則返回EBUSY
說明:
1. pthread_cond_wait 自動解鎖互斥量(如同執行了pthread_unlock_mutex),並等待條件變數觸發。這時執行緒掛起,不佔用CPU時間,直到條件變數被觸發(變數為ture)。在呼叫 pthread_cond_wait之前,應用程式必須加鎖互斥量。pthread_cond_wait函式返回前,自動重新對互斥量加鎖(如同執行了pthread_lock_mutex)。
2. 互斥量的解鎖和在條件變數上掛起都是自動進行的。因此,在條件變數被觸發前,如果所有的執行緒都要對互斥量加鎖,這種機制可保證線上程加鎖互斥量和進入等待條件變數期間,條件變數不被觸發。條件變數要和互斥量相聯結,以避免出現條件競爭——個執行緒預備等待一個條件變數,當它在真正進入等待之前,另一個執行緒恰好觸發了該條件(條件滿足訊號有可能在測試條件和呼叫pthread_cond_wait函式(block)之間被髮出,從而造成無限制的等待)。
3. 條件變數函式不是非同步訊號安全的,不應當在訊號處理程式中進行呼叫。特別要注意,如果在訊號處理程式中呼叫 pthread_cond_signal 或 pthread_cond_boardcast 函式,可能導致呼叫執行緒死鎖
示例程式碼1:
#include <stdio.h>
#include <pthread.h>
#include "stdlib.h"
#include "unistd.h"
pthread_mutex_t mutex;
pthread_cond_t cond;
void hander(void *arg)
{
free(arg);
(void)pthread_mutex_unlock(&mutex);
}
void *thread1(void *arg)
{
pthread_cleanup_push(hander, &mutex);
while(1)
{
printf("thread1 is running\n");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);
printf("thread1 applied the condition\n");
pthread_mutex_unlock(&mutex);
sleep(4);
}
pthread_cleanup_pop(0);
}
void *thread2(void *arg)
{
while(1)
{
printf("thread2 is running\n");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);
printf("thread2 applied the condition\n");
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main()
{
pthread_t thid1,thid2;
printf("condition variable study!\n");
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_create(&thid1,NULL,thread1,NULL);
pthread_create(&thid2,NULL,thread2,NULL);
sleep(1);
do{
pthread_cond_signal(&cond);
}while(1);
sleep(20);
pthread_exit(0);
return 0;
}
示例程式碼2:
#include <pthread.h>
#include <unistd.h>
#include "stdio.h"
#include "stdlib.h"
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
struct node
{
int n_number;
struct node *n_next;
}*head = NULL;
static void cleanup_handler(void *arg)
{
printf("Cleanup handler of second thread.\n");
free(arg);
(void)pthread_mutex_unlock(&mtx);
}
static void *thread_func(void *arg)
{
struct node *p = NULL;
pthread_cleanup_push(cleanup_handler, p);
while (1)
{
// 這個mutex主要是用來保證pthread_cond_wait的併發性。
pthread_mutex_lock(&mtx);
while (head == NULL)
{
/* 這個while要特別說明一下,單個pthread_cond_wait功能很完善,為何
* 這裡要有一個while (head == NULL)呢?因為pthread_cond_wait裡的線
* 程可能會被意外喚醒,如果這個時候head != NULL,則不是我們想要的情況。
* 這個時候,應該讓執行緒繼續進入pthread_cond_wait
* pthread_cond_wait會先解除之前的pthread_mutex_lock鎖定的mtx,
* 然後阻塞在等待對列裡休眠,直到再次被喚醒(大多數情況下是等待的條件成立
* 而被喚醒,喚醒後,該程序會先鎖定先pthread_mutex_lock(&mtx);,再讀取資源
* 用這個流程是比較清楚的。*/
pthread_cond_wait(&cond, &mtx);
p = head;
head = head->n_next;
printf("Got %d from front of queue\n", p->n_number);
free(p);
}
pthread_mutex_unlock(&mtx); // 臨界區資料操作完畢,釋放互斥鎖。
}
pthread_cleanup_pop(0);
return 0;
}
int main(void)
{
pthread_t tid;
int i;
struct node *p;
/* 子執行緒會一直等待資源,類似生產者和消費者,但是這裡的消費者可以是多個消費者,
* 而不僅僅支援普通的單個消費者,這個模型雖然簡單,但是很強大。*/
pthread_create(&tid, NULL, thread_func, NULL);
sleep(1);
for (i = 0; i < 10; i++)
{
p = (struct node*)malloc(sizeof(struct node));
p->n_number = i;
pthread_mutex_lock(&mtx); // 需要操作head這個臨界資源,先加鎖。
p->n_next = head;
head = p;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx); //解鎖
sleep(1);
}
printf("thread 1 wanna end the line.So cancel thread 2.\n");
/* 關於pthread_cancel,有一點額外的說明,它是從外部終止子執行緒,子執行緒會在最近的取消點,
* 退出執行緒,而在我們的程式碼裡,最近的取消點肯定就是pthread_cond_wait()了。*/
pthread_cancel(tid);
pthread_join(tid, NULL);
printf("All done -- exiting\n");
return 0;
}
可以看出,等待條件變數訊號的用法約定一般是這樣的:
...
pthread_mutex_lock(&mutex);
...
pthread_cond_wait (&cond, &mutex);
...
pthread_mutex_unlock (&mutex);
...
相信很多人都會有這個疑問:為什麼pthread_cond_wait需要的互斥鎖不在函式內部定義,而要使使用者定義的呢?現在沒有時間研究 pthread_cond_wait 的原始碼,帶著這個問題對條件變數的用法做如下猜測,希望明白真相看過原始碼的朋友不吝指正。
1. pthread_cond_wait 和 pthread_cond_timewait 函式為什麼需要互斥鎖?因為:條件變數是執行緒同步的一種方法,這兩個函式又是等待訊號的函式,函式內部一定有須要同步保護的資料。
2. 使用使用者定義的互斥鎖而不在函式內部定義的原因是:無法確定會有多少使用者使用條件變數,所以每個互斥鎖都須要動態定義,而且管理大量互斥鎖的開銷太大,使用使用者定義的即靈活又方便,符合UNIX哲學的程式設計風格(隨便推薦閱讀《UNIX程式設計哲學》這本好書!)。
3. 好了,說完了1和2,我們來自由猜測一下 pthread_cond_wait 函式的內部結構吧:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
{
if(沒有條件訊號)
{
(1)pthread_mutex_unlock (mutex); // 因為使用者在函式外面已經加鎖了(這是使用約定),但是在沒有訊號的情況下為了讓其他執行緒也能等待cond,必須解鎖。
(2) 阻塞當前執行緒,等待條件訊號(當然應該是類似於中斷觸發的方式等待,而不是軟體輪詢的方式等待)... 有訊號就繼續執行後面。
(3) pthread_mutex_lock (mutex); // 因為使用者在函式外面要解鎖(這也是使用約定),所以要與1呼應加鎖,保證使用者感覺依然是自己加鎖、自己解鎖。
}
...
}
三、 訊號量
如同程序一樣,執行緒也可以通過訊號量來實現通訊,雖然是輕量級的。
執行緒使用的基本訊號量函式有四個:
#include <semaphore.h>
1. 初始化訊號量
int sem_init (sem_t *sem , int pshared, unsigned int value);
引數:
sem - 指定要初始化的訊號量;
pshared - 訊號量 sem 的共享選項,linux只支援0,表示它是當前程序的區域性訊號量;
value - 訊號量 sem 的初始值。
2. 訊號量值加1
給引數sem指定的訊號量值加1。
int sem_post(sem_t *sem);
3. 訊號量值減1
給引數sem指定的訊號量值減1。
int sem_wait(sem_t *sem);
如果sem所指的訊號量的數值為0,函式將會等待直到有其它執行緒使它不再是0為止。
4. 銷燬訊號量
銷燬指定的訊號量。
int sem_destroy(sem_t *sem);
示例程式碼:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <errno.h>
#define return_if_fail(p) if((p) == 0){printf ("[%s]:func error!\n", __func__);return;}
typedef struct _PrivInfo
{
sem_t s1;
sem_t s2;
time_t end_time;
}PrivInfo;
static void info_init (PrivInfo* prifo);
static void info_destroy (PrivInfo* prifo);
static void* pthread_func_1 (PrivInfo* prifo);
static void* pthread_func_2 (PrivInfo* prifo);
int main (int argc, char** argv)
{
pthread_t pt_1 = 0;
pthread_t pt_2 = 0;
int ret = 0;
PrivInfo* prifo = NULL;
prifo = (PrivInfo* )malloc (sizeof (PrivInfo));
if (prifo == NULL)
{
printf ("[%s]: Failed to malloc priv.\n");
return -1;
}
info_init (prifo);
ret = pthread_create (&pt_1, NULL, (void*)pthread_func_1, prifo);
if (ret != 0)
{
perror ("pthread_1_create:");
}
ret = pthread_create (&pt_2, NULL, (void*)pthread_func_2, prifo);
if (ret != 0)
{
perror ("pthread_2_create:");
}
pthread_join (pt_1, NULL);
pthread_join (pt_2, NULL);
info_destroy (prifo);
return 0;
}
static void info_init (PrivInfo* prifo)
{
return_if_fail (prifo != NULL);
prifo->end_time = time(NULL) + 10;
sem_init (&prifo->s1, 0, 1);
sem_init (&prifo->s2, 0, 0);
return;
}
static void info_destroy (PrivInfo* prifo)
{
return_if_fail (prifo != NULL);
sem_destroy (&prifo->s1);
sem_destroy (&prifo->s2);
free (prifo);
prifo = NULL;
return;
}
static void* pthread_func_1 (PrivInfo* prifo)
{
return_if_fail (prifo != NULL);
while (time(NULL) < prifo->end_time)
{
sem_wait (&prifo->s2);
printf ("pthread1: pthread1 get the lock.\n");
sem_post (&prifo->s1);
printf ("pthread1: pthread1 unlock\n");
sleep (1);
}
return;
}
static void* pthread_func_2 (PrivInfo* prifo)
{
return_if_fail (prifo != NULL);
while (time (NULL) < prifo->end_time)
{
sem_wait (&prifo->s1);
printf ("pthread2: pthread2 get the unlock.\n");
sem_post (&prifo->s2);
printf ("pthread2: pthread2 unlock.\n");
sleep (1);
}
return;
}
四、非同步訊號
由於LinuxThreads是在核外使用核內輕量級程序實現的執行緒,所以基於核心的非同步訊號操作對於執行緒也是有效的。但同時,由於非同步訊號總是實際發往某個程序,所以無法實現POSIX標準所要求的"訊號到達某個程序,然後再由該程序將訊號分發到所有沒有阻塞該訊號的執行緒中"原語,而是隻能影響到其中一個執行緒。
POSIX非同步訊號同時也是一個標準C庫提供的功能,主要包括訊號集管理(sigemptyset()、sigfillset()、sigaddset()、sigdelset()、sigismember()等)、訊號處理函式安裝(sigaction())、訊號阻塞控制(sigprocmask())、被阻塞訊號查詢(sigpending())、訊號等待(sigsuspend())等,它們與傳送訊號的kill()等函式配合就能實現程序間非同步訊號功能。LinuxThreads圍繞執行緒封裝了sigaction()何raise(),本節集中討論LinuxThreads中擴充套件的非同步訊號函式,包括pthread_sigmask()、pthread_kill()和sigwait()三個函式。毫無疑問,所有POSIX非同步訊號函式對於執行緒都是可用的。
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask)
設定執行緒的訊號遮蔽碼,語義與sigprocmask()相同,但對不允許遮蔽的Cancel訊號和不允許響應的Restart訊號進行了保護。被遮蔽的訊號儲存在訊號佇列中,可由sigpending()函式取出。
int pthread_kill(pthread_t thread, int signo)
向thread號執行緒傳送signo訊號。實現中在通過thread執行緒號定位到對應程序號以後使用kill()系統呼叫完成傳送。
int sigwait(const sigset_t *set, int *sig)
掛起執行緒,等待set中指定的訊號之一到達,並將到達的訊號存入*sig中。POSIX標準建議在呼叫sigwait()等待訊號以前,程序中所有執行緒都應遮蔽該訊號,以保證僅有sigwait()的呼叫者獲得該訊號,因此,對於需要等待同步的非同步訊號,總是應該在建立任何執行緒以前呼叫pthread_sigmask()遮蔽該訊號的處理。而且,呼叫sigwait()期間,原來附接在該訊號上的訊號處理函式不會被呼叫。
如果在等待期間接收到Cancel訊號,則立即退出等待,也就是說sigwait()被實現為取消點。
五、 其他同步方式
除了上述討論的同步方式以外,其他很多程序間通訊手段對於LinuxThreads也是可用的,比如基於檔案系統的IPC(管道、Unix域Socket等)、訊息佇列(Sys.V或者Posix的)、System V的訊號燈等。只有一點需要注意,LinuxThreads在核內是作為共享儲存區、共享檔案系統屬性、共享訊號處理、共享檔案描述符的獨立程序看待的。
條件變數與互斥鎖、訊號量的區別
1.互斥鎖必須總是由給它上鎖的執行緒解鎖,訊號量的掛出即不必由執行過它的等待操作的同一程序執行。一個執行緒可以等待某個給定訊號燈,而另一個執行緒可以掛出該訊號燈。
2.互斥鎖要麼鎖住,要麼被解開(二值狀態,型別二值訊號量)。
3.由於訊號量有一個與之關聯的狀態(它的計數值),訊號量掛出操作總是被記住。然而當向一個條件變數傳送訊號時,如果沒有執行緒等待在該條件變數上,那麼該訊號將丟失。
4.互斥鎖是為了上鎖而設計的,條件變數是為了等待而設計的,訊號燈即可用於上鎖,也可用於等待,因而可能導致更多的開銷和更高的複雜性。
參考:http://blog.csdn.net/iw1210/article/details/8509629
也可參考: