文章目錄
- 1. 執行緒互斥
- 1.1 臨界資源、臨界區、原子性
- 1.2互斥量mutex
- 1.3互斥量的介面
- 1.4互斥量(鎖)實作原理
- 2. 可重入函式&&執行緒安全
- 2.1 常見的執行緒不安全的情況
- 3. 死鎖
- 3.1 死鎖四個必要條件
- 3.2 避免死鎖的方法
- 4.執行緒同步
- 4.1條件變數
- 4.2條件變數函式
- 4.3 為什么會有互斥鎖?
1. 執行緒互斥
- 互斥:任何時刻,互斥保證有且只有一個執行流進入臨界區,訪問臨界資源,通常對臨界資源起保護作用,
1.1 臨界資源、臨界區、原子性
- 臨界資源:被多個執行流同時訪問的共享資源就叫做臨界資源,
- 臨界區:每個執行緒內部,訪問臨界資源的代碼,就叫做臨界區,
- 原子性:不會被任何調度機制打斷的操作,該操作只有兩態,要么別做,要么做完,
1.2互斥量mutex
- 大部分情況,執行緒使用的資料都是區域變數,變數的地址空間在執行緒堆疊空間內,這種情況,變數歸屬單個執行緒,其他執行緒無法獲得這種變數,
- 但有時候,很多變數都需要在執行緒間共享,這樣的變數稱為共享變數,可以通過資料的共享,完成執行緒之間的互動,
- 多個執行緒并發的操作共享變數,會帶來一些問題,
下面寫個多個執行緒操作共享變數來搶票的售票系統代碼:


1.為什么可能無法獲得爭取結果?
if 陳述句判斷條件為真以后,代碼可以并發的切換到其他執行緒,usleep這個模擬漫長業務的程序中,可能有很多個執行緒會進入該代碼段,ticket--操作本身就不是一個原子操作,

要解決以上問題,需要做到三點:
- 代碼必須要有互斥行為:當代碼進入臨界區執行時,不允許其他執行緒進入該臨界區,
- 如果多個執行緒同時要求執行臨界區的代碼,并且臨界區沒有執行緒在執行,那么只能允許一個執行緒進入該臨界區,
- 如果執行緒不在臨界區中執行,那么該執行緒不能阻止其他執行緒進入臨界區,
要做到這三點,本質上就是需要一把鎖,Linux上提供的這把鎖叫互斥量(mutex),
1.3互斥量的介面
初始化互斥量:

銷毀互斥量:

銷毀互斥量需要注意:
- 不要銷毀一個已經加鎖的互斥量,
- 已經銷毀的互斥量,要確保后面不會有執行緒再嘗試加鎖,
互斥量加鎖和解鎖:

注意:在特定執行緒/行程擁有鎖的期間,有新的執行緒來申請鎖,pthread_ mutex_lock呼叫會陷入阻塞(執行流被掛起),等待互斥量解鎖,unlock之后對執行緒行程喚醒操作,此外,加鎖的粒度越小越好,
- 鎖的申請是將lock由1變為0;
- 鎖的銷毀時將lock由0變為1,
改進上面的搶票系統:


1.4互斥量(鎖)實作原理

每個執行緒的暫存器是私有的,在修改資料的時候用的不是拷貝而是xchgb交換,將暫存器的值和記憶體的值互換,這保證了鎖的原子性,因為其他執行緒申請的話,記憶體的值為0,申請不到鎖,lock:0表示被占, 1表示可以被申請,
- 整個程序為1的mutex只有一份,
- exchange一潭訓編完成了暫存器和記憶體資料的交換,
2. 可重入函式&&執行緒安全
- 執行緒安全:多個執行緒并發同一段代碼時,不會出現不同的結果,常見對全域變數或者靜態變數進行操作,并且沒有鎖保護的情況下,會出現該問題,
- 重入:同一個函式被不同的執行流呼叫,當前一個流程還沒有執行完,就有其他的執行流再次進入,我們稱之為重入,一個函式在重入的情況下,運行結果不會出現任何不同或者任何問題,則該函式被稱為可重入函式,否則,是不可重入函式,
2.1 常見的執行緒不安全的情況
- 不保護共享變數的函式
- 函式狀態隨著被呼叫,狀態發生變化的函式
- 回傳指向靜態變數指標的函式
- 呼叫執行緒不安全函式的函式
3. 死鎖
死鎖是指在一組行程中的各個行程均占有不會釋放的資源,但因為互相申請被其他行程所占用而不會釋放的資源,而處于的一種永久等待狀態,
3.1 死鎖四個必要條件
- 互斥條件:一個資源每次只能被一個執行流使用,
- 請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不放,
- 不剝奪條件:一個執行流已獲得的資源,在末使用完之前,不能強行剝奪,
- 回圈等待條件:若干執行流之間形成一種頭尾相接的回圈等待資源的關系,
3.2 避免死鎖的方法
- 破壞死鎖的四個必要條件
- 加鎖順序一致
- 避免鎖未釋放的
- 資源一次性分配
4.執行緒同步
同步概念:在保證資料安全(一般使用加鎖方式)的情況下,讓執行緒能夠按照某種特定的順序訪問臨界資源,就叫做同步,
- 為什么要存在同步?
- 使多執行緒同步高效的完成某些事情,
同步實作的事情:當有資源的時候,可以直接獲取資源,沒有資源的時候,執行緒進行等待,等待另外的執行緒生產一個資源,當生產完成的時候,通知等待的執行緒,
4.1條件變數
- 當一個執行緒互斥地訪問某個變數時,它可能發現在其它執行緒改變狀態之前,它什么也做不了,
- 例如一個執行緒訪問佇列時,發現佇列為空,它只能等待,只到其它執行緒將一個節點添加到佇列中,這種情況就需要用到條件變數,
競態條件:因為時序問題,而導致程式例外,我們稱之為競態條件,在執行緒場景下,這種問題也不難理解,
條件變數的本質:PCB等待佇列+兩個介面(等待介面+喚醒介面)
4.2條件變數函式
- 定義條件變數
pthread_cond_t 條件變數型別
- 初始化條件變數
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
引數:
cond:傳入條件變數的地址
attr:條件變數的屬性,一般設定為NULL,采用默認屬性
- 銷毀(釋放動態初始化的條件變數所占用的記憶體)
int pthread_cond_destroy(pthread_cond_t *cond)
- 等待條件滿足(將呼叫該等待介面的執行流放入PCB等待佇列當中)
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
引數:
cond:傳入條件變數的地址
restrict mutex:傳入互斥鎖變數的地址
- 喚醒等待(通知PCB等待當中的執行流來訪問臨界資源)
int pthread_cond_signal(pthread_cond_t *cond);
引數:
cond:傳入條件變數的地址
//喚醒至少一個PCB等待佇列當中的執行緒
4.3 為什么會有互斥鎖?
- 同步并沒有保證互斥,意味著不同的執行流可以在同一時刻去訪問臨界資源,所以需要條件變數中的互斥鎖來保證互斥,各執行流在訪問臨界資源的時候,只有一個執行流可以訪問,

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t lock;
pthread_cond_t cond;
void *task_t2(void *arg)
{
const char *name = (char*)arg;
while(1){
pthread_cond_wait(&cond, &lock);
printf("get cond : %s 活動...\n", name);
}
}
void *task_t1(void *arg)
{
const char *name = (char*)arg;
while(1){
sleep(rand()%3+1);
pthread_cond_signal(&cond);
printf("%s signal done...\n", name);
}
}
int main()
{
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&cond, NULL);
pthread_t t1,t2,t3,t4,t5;
pthread_create(&t1, NULL, task_t1, "thread1");
pthread_create(&t2, NULL, task_t2, "thread2");
pthread_create(&t3, NULL, task_t2, "thread3");
pthread_create(&t4, NULL, task_t2, "thread4");
pthread_create(&t5, NULL, task_t2, "thread5");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_join(t5, NULL);
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/279365.html
標籤:其他
上一篇:大話測驗六技——讓測驗不要太容易
下一篇:執行緒
