目錄
1、執行緒的互斥
2、可重入VS執行緒安全
3、執行緒的同步
1、執行緒的互斥
1)執行緒互斥的相關概念
- 臨界資源:被多個執行流共享的資源就稱為臨界資源,例如全域變數,
- 臨界區:訪問臨界資源的代碼稱為臨界區,
- 互斥:互斥保證了任何時刻只有一個執行緒進入臨界區訪問臨界資源,
- 原子性:不會被任何機制打斷的操作,該操作只有兩態,要么已經完成要么還沒開始(不能存在已經開始了,但是還沒完成的情況,簡單理解就是依據匯編代碼就可以實作的),

2)通過訂票示例引入互斥量
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int tickets = 1000;//票數,每訂購一張票數減1
void* ticket(void* arg)
{
//模擬訂票程序,多個執行緒(執行流)訪問該程式
while(1)
{
if(tickets > 0)
{
usleep(500);
printf("%s,搶票成功,剩余票數:%d\n",(char*)arg,--tickets);
}
else
break;
} }
int main()
{
//創建多個執行緒
pthread_t tid[5];
pthread_create(&tid[0],NULL,ticket,"new thread1");
pthread_create(&tid[1],NULL,ticket,"new thread2");
pthread_create(&tid[2],NULL,ticket,"new thread3");
pthread_create(&tid[3],NULL,ticket,"new thread4");
pthread_create(&tid[4],NULL,ticket,"new thread5");
//執行緒等待
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
pthread_join(tid[2],NULL);
pthread_join(tid[3],NULL);
pthread_join(tid[4],NULL);
return 0;
}
運行結果分析:

第一個原因:當一個執行流執行到if判斷為真以后,首先會usleep(500),在這期間ticket還沒有進行--操作,因此ticket還是大于0的,如果這時再有其他執行流執行if還是判斷為真,然后等待一段時間多個執行流對ticket(此時值為0)進行--就變成了負值,

第二個原因:ticket--本身就不是原子性的,匯編代碼如下:

如何解決這個問題?
- 代碼必須要有互斥行為:當代碼進入臨界區執行時,不允許其他執行緒進入該臨界區執行
- 如果多個執行緒同時要進入臨界區執行,且當前臨界區沒有執行緒在執行時,只允許一個執行緒進入該臨界區進行執行,
- 如果執行緒不在臨界區執行,或者已經從臨界區執行完了,則不能組織其他執行緒進入臨界區執行,
滿足這些條件的實際上就是一把鎖,Linux中提供的鎖叫做互斥量,注意:要保證加鎖后其他執行緒不能進入臨界區,則其他執行緒必須能夠看到鎖,也就是說鎖也是臨界資源(多個執行緒都可以訪問),因此鎖在保證臨界區前必須先保證自己的安全,也就是說加鎖的程序必須是原子性的(保證了多個執行緒進入該臨界區時只允許一個進入),

3)互斥量的相關介面
初始化互斥量
方法1:靜態分配(全域變數)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;
方法2:動態分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
引數:mutex需要初始化的互斥量 attr暫不關注,置NULL即可
回傳值:成功回傳0,失敗回傳錯誤碼
注意:如果使用方法1初始化的互斥量不需要銷毀
銷毀互斥量
int pthread_mutex_destroy(pthread_mutex_t *restrict mutex);
注意:不要銷毀一個已經加鎖的互斥量;已經銷毀的互斥量,要確保后面不會在嘗試進行加鎖,
互斥量加鎖和解鎖
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
使用互斥量改進售票系統代碼
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
pthread_mutex_t mutex;//初始化互斥量
int tickets = 10000;//票數,每訂購一張票數減1
void* ticket(void* arg)
{
//模擬訂票程序,多個執行緒(執行流)訪問該程式
while(1)
{
pthread_mutex_lock(&mutex);//加鎖
if(tickets > 0)
{
printf("%s,搶票成功,剩余票數:%d\n",(char*)arg,--tickets);
pthread_mutex_unlock(&mutex);//解鎖
usleep(500);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main()
{
//創建多個執行緒
pthread_t tid[5];
pthread_create(&tid[0],NULL,ticket,"new thread1");
pthread_create(&tid[1],NULL,ticket,"new thread2");
pthread_create(&tid[2],NULL,ticket,"new thread3");
pthread_create(&tid[3],NULL,ticket,"new thread4");
pthread_create(&tid[4],NULL,ticket,"new thread5");
//執行緒等待
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
pthread_join(tid[2],NULL);
pthread_join(tid[3],NULL);
pthread_join(tid[4],NULL);
//銷毀互斥量
pthread_mutex_destroy(&mutex);
return 0;
}
4)互斥量原理

語言層面單純的i++/++i等并不是具有原子性,可能會有資料一致性問題,為了實作互斥鎖操作,大多數體系結構都實作了swap或exchange指令,該指令的作用是把暫存器和記憶體單元的資料相互交換,由于只有一跳指令,保證了原子性,

2、可重入VS執行緒安全
1)相關概念
- 執行緒安全:多個執行緒并發執行同一段代碼不會出現不同結果,常見對全域變數或靜態變數進行操作,并且沒有鎖保護的情況下,會出現該問題,
- 可重入:同一個函式被不同的執行流執行,當一個執行流還沒有結束,其他執行流就再次進入,我們稱之為重入,一個函式在重入的情況下,運行結果不會出現任何不同或任何問題的情況下,我們稱之為可重入,反之,稱之為不可重入,
2)常見執行緒不安全的情況
-
不保護共享變數的函式
-
函式狀態隨著被呼叫,狀態發生變化的函式
-
回傳指向靜態變數指標的函式
-
呼叫執行緒不安全函式的函式
3)常見的執行緒安全的情況
-
每個執行緒對全域變數或者靜態變數只有讀取的權限,而沒有寫入的權限,一般來說這些執行緒是安全的
-
類或者介面對于執行緒來說都是原子操作
-
多個執行緒之間的切換不會導致該介面的執行結果存在二義性
4)常見不可重入的情況
-
呼叫了malloc/free函式,因為malloc函式是用全域鏈表來管理堆的
-
呼叫了標準I/O庫函式,標準I/O庫的很多實作都以不可重入的方式使用全域資料結構
-
可重入函式體內使用了靜態的資料結構
5)常見可重入的情況
-
不使用全域變數或靜態變數
-
不使用用malloc或者new開辟出的空間
-
不呼叫不可重入函式
-
不回傳靜態或全域資料,所有資料都有函式的呼叫者提供
- 使用本地資料,或者通過制作全域資料的本地拷貝來保護全域資料
6)可重入與執行緒安全的區別與聯系
-
函式是可重入的,那就是執行緒安全的
-
函式是不可重入的,那就不能由多個執行緒使用,有可能引發執行緒安全問題
-
如果一個函式中有全域變數,那么這個函式既不是執行緒安全也不是可重入的,
-
可重入函式是執行緒安全函式的一種
-
執行緒安全不一定是可重入的,而可重入函式則一定是執行緒安全的,
- 如果將對臨界資源的訪問加上鎖,則這個函式是執行緒安全的,但如果這個重入函式若鎖還未釋放則會產生死鎖,因此是不可重入的
3、執行緒的同步
1)常見鎖的概念
死鎖是指在在一組行程中的各個行程均占有不會釋放的資源,但因互相申請被其他行程占用的不會釋放的資源而處于一種永久等待的狀態,
死鎖的四個必要條件
- 互斥條件:一個資源一次只能被一個執行流使用,
- 請求與保持條件:一個執行流因請求資源而等待時,對已申請到的資源保持不放,
- 不剝奪條件:一個執行流已獲得的資源,在未使用完之前不能強行剝奪,
- 回圈等待條件:若干執行流之間形成一種頭尾相接的回圈等待資源的關系,
死鎖的避免
- 破壞死鎖的四個條件
- 加鎖順序一致
- 避免鎖未釋放的場景
- 資源一次性分配
死鎖的避免演算法
- 四所檢測演算法
- 銀行家演算法
3)Linux執行緒同步相關概念
- 條件變數:當一個執行緒互斥地訪問某個變數時,它可能發現在其它執行緒改變狀態之前,它什么也做不了,例如一個執行緒訪問佇列時,發現佇列為空,它只能等待,只到其它執行緒將一個節點添加到佇列中,這種情況就需要用到條件變數
- 同步概念:在保證資料安全的前提下,讓執行緒能夠按照某種特定的順序訪問臨界資源,從而有效避免饑餓問題,叫做同步,
- 競態條件:因為時序問題,而導致程式例外,我們稱之為競態條件,
4)什么是執行緒同步?為什么要存在同步?
在保證執行緒安全的條件下,讓多個執行流按照特定的順序訪問臨界資源,我們稱之為同步,

執行緒同步保證了多個執行緒協同完成任務的安全性和高效性,
5)執行緒同步相關介面
初始化條件變數
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
引數:cond要初始化的條件變數 attr:暫不關注,置空即可
回傳值:成功回傳0,失敗回傳錯誤碼
銷毀條件變數
int pthread_cond_desroy(pthread_cond_t *restrict cond);//銷毀條件變數
等待條件滿足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
引數:cond等待的條件變數 mutex:互斥量
喚醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
void* fun1(void* arg)
{
while(1)
{
sleep(1);//每隔一秒發送一個信號
pthread_cond_signal(&cond);
}
}
void* fun2(void* arg)
{
while(1)
{
//等待,當接收到信號就會執行下面輸出陳述句,
pthread_cond_wait(&cond,&mutex);
printf("開始行動\n");
}
}
int main()
{
//初始化互斥量和條件變數
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
//創建兩個執行緒,一個發送信號,另一個接收到信號列印“活動”
pthread_t t1,t2;
pthread_create(&t1,NULL,fun1,NULL);
pthread_create(&t2,NULL,fun2,NULL);
//執行緒等待
pthread_join(t1,NULL);
pthread_join(t2,NULL);
//銷毀互斥量和條件變數
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
6)為什么pthread_cond_wait需要互斥量
- 條件等待是執行緒間同步的一種手段,如果只有一個執行緒,條件不滿足時一直等下去條件都不會滿足,所以必須要有一個執行緒通過某些操作,改變共享變數,使得原先不滿足的條件變得滿足并去通知等待的執行緒,
- 條件變數的滿足必然會牽扯到共享資料的變化,所以必須有互斥鎖的保護,沒有互斥鎖就無法保證執行緒安全的獲取修改條件變數,
- 當一個執行緒發現條件不滿足時,就要呼叫wait將自己掛起等待,掛起等待時是帶鎖等待的!!!如果不解鎖,其他執行緒無法訪問條件變數,條件永遠也不會成立,該執行緒將一直等待下去,因此,這里的互斥量起到解鎖的作用,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/279368.html
標籤:其他
上一篇:暴力破解原理及實驗
下一篇:極客日報第109期:蘋果合作伙伴廣達遭黑客勒索 5000 萬美元;女子5年前打賭錘子手機破產贏一部iPhone;?安卓32位時代落幕
