我們都知道,進程是操作系統對運行程序資源分配的基本單位,而線程是程序邏輯,調用的基本單位。在多線程的程序中,多個線程共享臨界區資源,那麼就會有問題:
比如
#include <pthread.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int g_val = 10; void * test1(void* args) { g_val = 20; printf("in %s: g_val = %d\n",__func__, g_val); } void * test2(void* args) { sleep(1); printf("in %s: g_val = %d\n",__func__,g_val); } int main(int argc, char const *argv[]) { pthread_t id1,id2; pthread_create(&id1,NULL,test1,NULL); pthread_create(&id2,NULL,test2,NULL); pthread_join(id1,NULL); pthread_join(id2,NULL); return 0; }
由次我們可以看到,線程1修改了全局變量,而線程2中頁跟著改變了。
那麼,對於這個問題進行放大,我們就會找到多線程存在的問題。如下
#include <stdio.h> #include <pthread.h> // pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; int g_val = 0; void* add(void *argv) { for(int i = 0 ; i < 5000; ++i) { // g_val++; // pthread_mutex_lock(&lock); int tmp = g_val; g_val = tmp+1; // pthread_mutex_unlock(&lock); } } int main(int argc, char const *argv[]) { pthread_t id1,id2; pthread_create(&id1,NULL,add,NULL); pthread_create(&id2,NULL,add,NULL); pthread_join(id1,NULL); pthread_join(id2,NULL); printf("%d\n",g_val); return 0; }
在上面代碼中,我們執行兩個線程分別對全局變量累加5000次,但是得到的結果卻是不確定的。這是因為,在多線程程序中,線程調度使得線程間進行切換執行,如果當線程1將數據從內存讀入cpu正在准備累加時,調度器切換線程2執行,此時,線程2獲取的值是未累加的。那麼,當兩個線程都執行完本次累加後,實際值只增加了1。所以就會產生多次執行,結果不確定性。
注:代碼中沒有直接g_val++,而選擇了tmp過度就是為了產生非原子操作,讓調度過程處於累加未完時。
那麼解決這個問題,就需要互斥操作了。
我們首先來談互斥量mutex
通過互斥量實現線程鎖,在每個線程累加之前,進行臨界資源的鎖操作,在結束時解鎖,那麼就能保證目標的實現了。
#include <stdio.h> #include <pthread.h> pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; int g_val = 0; void* add(void *argv) { for(int i = 0 ; i < 5000; ++i) { // g_val++; pthread_mutex_lock(&lock); int tmp = g_val; g_val = tmp+1; pthread_mutex_unlock(&lock); } } int main(int argc, char const *argv[]) { pthread_t id1,id2; pthread_create(&id1,NULL,add,NULL); pthread_create(&id2,NULL,add,NULL); pthread_join(id1,NULL); pthread_join(id2,NULL); printf("%d\n",g_val); return 0; }
關於互斥鎖的實現,在linux中實現如下
問題場景描述
假設我們現在需要做一個生產者消費者模型,生產者對帶有頭節點的鏈表頭插方式push_front生產數據,消費者調用pop_front消費數據.而生產者可能動作比較慢,這時就會有問題。
生產者生產一個數據時間,消費者可能迫切需求。因此,一直輪尋申請鎖資源,以便進行消費。所以就會產生多次不必的鎖資源申請釋放動作。影響系統性能。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; typedef struct node { int _data; struct node *_next; }node_t,* node_p,**node_pp; node_p head = NULL; node_p alloc_node(int data) { node_p ret = (node_p)malloc(sizeof(node_t)); ret->_data = data; ret->_next = NULL; return ret; } void init(node_pp phead) { *phead = alloc_node(0); } void push_front(node_p head,int data) { node_p tmp = alloc_node(data); tmp->_next = head->_next; head->_next = tmp; } void pop_front(node_p head, int * pdata) { if(head->_next!=NULL) { node_p tmp = head->_next; head->_next = tmp->_next; *pdata = tmp->_data; free(tmp); } } void show(node_p head) { node_p cur = head->_next; while(cur) { printf("%d->", cur->_data); cur = cur->_next; } printf("\n"); } //消費者 void * consumer(void *argv) { int data; while(1) { pthread_mutex_lock(&lock); // while(head->_next==NULL) if(head->_next==NULL) { printf("producter is not ready\n"); // pthread_cond_wait(&cond,&lock); // break; } else{ printf("producter is ready...\n"); pop_front(head,&data); printf("%s data = %d \n",__func__, data); } pthread_mutex_unlock(&lock); sleep(1); } } void * producter(void * argv) { int data = rand()%1234; while(1) { sleep(4); pthread_mutex_lock(&lock); push_front(head,data); printf("%s data :: %d\n",__func__, data); pthread_mutex_unlock(&lock); // pthread_cond_signal(&cond); } } int main(int argc, char const *argv[]) { init(&head); pthread_t id1,id2; pthread_create(&id1,NULL,consumer,NULL); pthread_create(&id2,NULL,producter,NULL); pthread_join(id1,NULL); pthread_join(id2,NULL); }
由上,我們發現。生產者生叉一個數據之後,消費者總是會多次進行鎖資源申請並嘗試消費數據。那麼,解決這一問題的方案就是:條件變量。
具體實現如下:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; typedef struct node { int _data; struct node *_next; }node_t,* node_p,**node_pp; node_p head = NULL; node_p alloc_node(int data) { node_p ret = (node_p)malloc(sizeof(node_t)); ret->_data = data; ret->_next = NULL; return ret; } void init(node_pp phead) { *phead = alloc_node(0); } void push_front(node_p head,int data) { node_p tmp = alloc_node(data); tmp->_next = head->_next; head->_next = tmp; } void pop_front(node_p head, int * pdata) { if(head->_next!=NULL) { node_p tmp = head->_next; head->_next = tmp->_next; *pdata = tmp->_data; free(tmp); } } void show(node_p head) { node_p cur = head->_next; while(cur) { printf("%d->", cur->_data); cur = cur->_next; } printf("\n"); } //消費者 void * consumer(void *argv) { int data; while(1) { pthread_mutex_lock(&lock); while(head->_next==NULL) // if(head->_next==NULL) { printf("producter is not ready\n\n"); pthread_cond_wait(&cond,&lock); break; } // else{ // printf("producter is ready...\n"); pop_front(head,&data); printf("%s data = %d \n",__func__, data); // } pthread_mutex_unlock(&lock); sleep(1); } } void * producter(void * argv) { int data = rand()%1234; while(1) { sleep(4); pthread_mutex_lock(&lock); push_front(head,data); printf("%s data :: %d\n",__func__, data); pthread_mutex_unlock(&lock); pthread_cond_signal(&cond); //條件變量v操作 } } int main(int argc, char const *argv[]) { init(&head); pthread_t id1,id2; pthread_create(&id1,NULL,consumer,NULL); pthread_create(&id2,NULL,producter,NULL); pthread_join(id1,NULL); pthread_join(id2,NULL); }
由圖可以看出,這下我們的消費者不再進行過多次沒必要的輪尋訪問,當生產者生產數據時,告訴消費者可以進行消費了,那麼消費者進行消費。
其實這也就是著名的:好萊塢原則---不要打電話給我們,我們會通知你。
eg,在面試筆試中,我們不需要過度的緊張是否被錄用,只需要在做到最大努力之後等著招聘方通知就好。
注:一個Condition Variable總是和一個Mutex搭配使用的。一個線程可以調用
pthread_cond_wait在一一個Condition Variable上阻塞等待,這個函數做以下三步操作:
1. 釋放Mutex
2. 阻塞等待
3. 當被喚醒時,重新獲得Mutex並返回