標籤:線程同步
線程最大的特點就是資源的共用性,所以也就有了一個痛點線程同步,實現線程同步的方法最常用的方法是:互斥鎖,條件變數和訊號量。接下來就讓我們來看下這幾種同步的方法。
一、互斥鎖(Mutex)
獲得鎖的線程可以完成“讀-修改-寫”的操作,然後釋放鎖給其它線程,沒有獲得鎖的線程只能等待而不能訪問共用資料,這樣“讀-修改-寫”三步操作組成一個原子操作,要麼都執行,要麼都不執行,不會執行到中間被打斷,也不會在其它處理器上並行做這個操作。
1、鎖的初始化和銷毀(Mutex用pthread_mutex_t類型的變數表示)
650) this.width=650;" src="https://s4.51cto.com/wyfs02/M02/A7/6C/wKioL1nmpo_Swv8XAABm7CySx6g673.png" title="mutex.png" alt="wKioL1nmpo_Swv8XAABm7CySx6g673.png" />
注意:如果Mutex變數是靜態分配的(全域變數 或static變數),也可以用宏PTHREAD_MUTEX_INITIALIZER來初始化,相當於用pthread_mutex_init初始化並且attr參數為NULL。用函數初始化則是動態分配。
2、加鎖解鎖
650) this.width=650;" src="https://s5.51cto.com/wyfs02/M01/A7/6C/wKioL1nmp7GwBHHHAABTcKT1dPo031.png" title="mutex.png" alt="wKioL1nmp7GwBHHHAABTcKT1dPo031.png" />
一個線程可以調用pthread_mutex_lock獲得Mutex,如果這時另一個線程已經調用pthread_mutex_lock獲得了該Mutex,則當前線程需要掛起等待,直到另一個線程調用
pthread_mutex_unlock釋放Mutex,當前線程被喚醒,才能獲得該Mutex並繼續執行。如果一個線程既想獲得鎖,又不想掛起等待,可以調用pthread_mutex_trylock,如果Mutex已經被另一個線程獲得,這個函數會失敗返回EBUSY,而不會使線程掛起等待。
#include<stdio.h>#include<stdlib.h>#include<pthread.h>#define LOOP 5000static int count = 0;pthread_mutex_t mutex_lock = PTHREAD_MUTEX_INITIALIZER;void *read_write(void *val){int _val = 0;int i = 0;for( ; i < LOOP; i++ ){pthread_mutex_lock(&mutex_lock);_val = count;printf("pthread id is :%x,count : %d\n",(unsigned long)pthread_self(),count);count = _val+1;pthread_mutex_unlock(&mutex_lock);}}int main(){pthread_t tid1;pthread_t tid2;pthread_create(&tid1,NULL,read_write,NULL);//為了簡單沒有判斷是否建立成功pthread_create(&tid2,NULL,read_write,NULL);pthread_join(tid1,NULL);pthread_join(tid2,NULL);printf("count final value is : %d\n",count);return 0;}
如果沒有加入互斥鎖,運行結果不會是10000。
3、互斥鎖的缺點
死結的情況:1)一般情況下,如果同一個線程先後兩次調用lock,在第二次調用時,由於鎖已經被佔用,該線程會掛起等待別的線程釋放鎖,然而鎖正是被自己佔用著的,該線程又被掛起而沒有機會釋放鎖,因此就永遠處於掛起等待狀態了。2)線程A獲得了鎖1,線程B獲得了鎖2,這時線程A調用lock試圖獲得鎖2,結果是需要掛起等待線程B釋放鎖2,而這時線程B也調用lock試圖獲得鎖1,結果是需要掛起等待線程A釋放鎖1,於是線程A和B都永遠處於掛起狀態了。
解決辦法:首先要盡量避免同時獲得多個鎖。如果所有線程在需要多個鎖時都按相同的先後順序(常見的是按Mutex變數的地址順序)獲得鎖,則不會出現死結。
二、條件變數
互斥鎖不同,條件變數是用來等待而不是用來上鎖的。條件變數用來自動阻塞一個線程,直到某特殊情況發生為止。通常條件變數和互斥鎖同時使用。條件變數分為兩部分: 條件和變數。條件本身是由互斥量保護的。線程在改變條件狀態前先要鎖住互斥量。條件變數使我們可以睡眠等待某種條件出現。條件變數是利用線程間共用的全域變數進行同步的一種機制,主要包括兩個動作:一個線程等待"條件變數的條件成立"而掛起;另一個線程使"條件成立"(給出條件成立訊號)。條件的檢測是在互斥鎖的保護下進行的。如果一個條件為假,一個線程自動阻塞,並釋放等待狀態改變的互斥鎖。如果另一個線程改變了條件,它發訊號給關聯的條件變數,喚醒一個或多個等待它的線程,重新獲得互斥鎖,重新評價條件。如果兩進程共用可讀寫的記憶體,條件變數可以被用來實現這兩進程間的線程同步。
1、條件變數的初始化和銷毀
650) this.width=650;" src="https://s5.51cto.com/wyfs02/M00/08/BE/wKiom1nms0jBDzejAABgwD25S88063.png" title="tiaojian.png" alt="wKiom1nms0jBDzejAABgwD25S88063.png" />
條件變數初始化和互斥鎖初始化類似,宏表示靜態分配,函數表示動態分配,當函數的第二個參數為NULL則兩者等價。
2、等待條件成立和啟用條件變數
650) this.width=650;" src="https://s3.51cto.com/wyfs02/M00/08/BE/wKiom1nmtcWTdwg6AAByOUDxKcs887.png" title="wait.png" alt="wKiom1nmtcWTdwg6AAByOUDxKcs887.png" />
650) this.width=650;" src="https://s1.51cto.com/wyfs02/M00/A7/6E/wKioL1nmskCzx6YlAAA7Ziy_vK0848.png" title="signe.png" alt="wKioL1nmskCzx6YlAAA7Ziy_vK0848.png" />
可見,一個條件變數總是和一個Mutex搭配使用的。 一個線程可以調用pthread_cond_wait在一個條件變數上阻塞等待,這個函數做以下三步操作:
1)釋放Mutex
2)阻塞等待
3)當被喚醒時,重新獲得Mutex並返回
pthread_cond_timedwait函數還有一個額外的參數可以設定等待逾時,如果到達了abstime所指定的時刻仍然沒有別的線程來喚醒當前線程,就返回ETIMEDOUT。一個線程可以調用pthread_cond_signal喚醒在某個條件變數上等待的另一個線程,也可以調用pthread_cond_broadcast喚醒在這個條件變數上等待的所有線程。
下面寫一個生產者和消費者的實現(鏈表):
#include<stdio.h>#include<stdlib.h>#include<pthread.h>typedef struct list{struct list *next;int val;}product_list;product_list *head = NULL;static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;static pthread_cond_t need_product=PTHREAD_COND_INITIALIZER;/*init list*/void init_list(product_list *list){if(NULL != list){list->next = NULL;list->val = 0;}}/*consumer*/void *consumer(void *_val){product_list *p = NULL;while(1){pthread_mutex_lock(&lock);while(NULL == head){pthread_cond_wait(&need_product,&lock);}p = head;head = head->next;p->next = NULL;pthread_mutex_unlock(&lock);printf("consum success,val is : %d\n",p->val);free(p);p = NULL;}}/*product*/void *product(void *_val){while(1){sleep(rand()%2);product_list *p = (product_list*)malloc(sizeof(product_list));pthread_mutex_lock(&lock);init_list(p);p->val = rand()%1000;p->next = NULL;head = p;pthread_mutex_unlock(&lock); printf("product success,val : %d\n",p->val);pthread_cond_signal(&need_product);}}int main(){pthread_t t_product;pthread_t t_consumer;pthread_create(&t_product,NULL,product,NULL);//no check pthread_create(&t_consumer,NULL,consumer,NULL);//no checkpthread_join(t_product,NULL);//wait product threadpthread_join(t_consumer,NULL);//wait consumer threadreturn 0;}
650) this.width=650;" src="https://s3.51cto.com/wyfs02/M01/A7/6F/wKioL1nmvBqzoRzKAABvD0kOj9U458.png" title="jieguo.png" alt="wKioL1nmvBqzoRzKAABvD0kOj9U458.png" />
三、訊號量
Mutex變數是非0即1的,可看作一種資源的可用數量,初始化時Mutex是1,表示有一個可用資源,加鎖時獲得該資源,將Mutex減到0,表示不再有可用資源,解鎖時釋放該資源,將Mutex重新加到1,表示又有了一個可用資源。
訊號量(Semaphore)和Mutex類似,表示可用資源的數量,和Mutex不同的是這個數量可以大於1。即,如果訊號量描述的資源數目是1時,此時的訊號量和互斥鎖相同!POSIX semaphore庫函數,這種訊號量不僅可用於同一進程的線程間同步,也可用於不同進程間的同步。
1、訊號量的建立和銷毀
int sem_init(sem_t *sem , int pshared, unsigned int value);int sem_destroy(sem_t *sem);
2、訊號量的等待和釋放
int sem_wait(sem_t *sem);//阻塞等待int sem_trywait(sem_t *sem);//非阻塞等待
調用sem_wait()可以獲得資源(P操作),使semaphore的值減1,如果調用sem_wait()時semaphore的值已經是0,則掛起等待。如果不希望掛起等待,可以調用sem_trywait() 。調用sem_post() 可以釋放資源(V操作),使semaphore 的值加1,同時喚醒掛起等待的線程。
下面寫一個生產者和消費者的實現(固定大小迴圈隊列):
#include<stdio.h>#include<stdlib.h>#include<pthread.h>#include<semaphore.h>#define SEM_PRO 10#define SEM_COM 0/*定義訊號量*/sem_t sem_product;sem_t sem_consume;int bank[SEM_PRO];/*消費者線程執行函數*/void *consumer(void *_val){int c = 0;while(1){sem_wait(&sem_consume);int _consume = bank[c];printf("consumer done...,val : %d\n",_consume);sem_post(&sem_product);c = (c+1)%SEM_PRO;sleep(rand()%5);}}/*生產者線程執行函數*/void *producter(void *_val){int p = 0;while(1){sem_wait(&sem_product);int _product = rand()%100;bank[p] = _product;printf("product done... val is : %d\n",_product);sem_post(&sem_consume);p = (p+1)%SEM_PRO;sleep(rand()%3);}}/*建立生產者消費者線程*/void run_product_consume(){pthread_t tid_consumer;pthread_t tid_producter;pthread_create(&tid_consumer,NULL,consumer,NULL);pthread_create(&tid_producter,NULL,producter,NULL); pthread_join(tid_consumer,NULL);pthread_join(tid_producter,NULL);}/*銷毀訊號量*/void destroy_all_sem(){printf("process done...\n");sem_destroy(&sem_product);sem_destroy(&sem_consume);exit(0);}/*初始化訊號量*/void init_all_sem(){int bank[SEM_PRO];int num = sizeof(bank)/sizeof(bank[0]);int i = 0;for(; i < num; i++ ){bank[i] = 0;}sem_init(&sem_product,0,SEM_PRO);sem_init(&sem_consume,0,SEM_COM);}int main(){init_all_sem();run_product_consume();//destroy_all_sem();//避免銷毀訊號量return 0;}
650) this.width=650;" src="https://s5.51cto.com/wyfs02/M02/A7/71/wKioL1nmyyTDE2GvAABrfUyaGA8668.png" title="哈哈.png" alt="wKioL1nmyyTDE2GvAABrfUyaGA8668.png" />
Linux多線程之線程同步