目錄
一.linux互斥
1.進(jìn)程線程間的互斥相關(guān)背景概念
2.互斥量mutex
3.加鎖互斥鎖mutex
4.鎖的底層原理
?二.可重入VS線程安全
1.概念
2.常見的線程不安全的情況
3.常見的線程安全的情況?
4.常見不可重入的情況?
5..常見可重入的情況
6.可重入與線程安全聯(lián)系
?三.死鎖
1.死鎖四個必要條件
2.避免死鎖
3.避免死鎖算法
四.Linux線程同步
1.條件變量
2.同步概念與競態(tài)條件
?3.條件變量函數(shù)
4.代碼樣例
一.linux互斥
1.進(jìn)程線程間的互斥相關(guān)背景概念
- 臨界資源:多線程執(zhí)行流共享的資源就叫做臨界資源。
- 臨界區(qū):每個線程內(nèi)部,訪問臨界資源的代碼,就叫做臨界區(qū)。
- 互斥:任何時刻,互斥保證有且只有一個執(zhí)行流進(jìn)入臨界區(qū),訪問臨界資源,通常對臨界資源起保護(hù)作用。
- 原子性(后面討論如何實現(xiàn)):不會被任何調(diào)度機(jī)制打斷的操作,該操作只有兩態(tài),要么完成,要么未完成。
2.互斥量mutex
大部分情況,線程使用的數(shù)據(jù)都是局部變量,變量的地址空間在線程??臻g內(nèi),這種情況,變量歸屬單個線程,其他線程無法獲得這種變量。
但有時候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過數(shù)據(jù)的共享,完成線程之間的交互。
多個線程并發(fā)的操作共享變量,會帶來一些問題:
測試代碼:
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;
#define NUM 4 // 線程數(shù)
int ticket = 1000; // 1000票
void *RobTicket(void *args)
{
const char *name = static_cast<const char *>(args);
while (1)
{
if (ticket > 0)
{
usleep(2000);
printf("%s-ticket,%d\n", name, ticket);
ticket--; // 四個線程同時對ticket--,直到ticket為0時結(jié)束
}
else
{
break;
}
usleep(100);
}
}
int main()
{
pthread_t tid[NUM];
for (int i = 0; i < NUM; i++)
{
char *name = new char[50];
sprintf(name, "Thread-%d", i + 1);
pthread_create(tid + i, NULL, RobTicket, name);
}
for (int i = 0; i < NUM; i++)
{
pthread_join(tid[i], NULL);
}
return 0;
}
測試結(jié)果:
?說明:
- 由于 if 語句判斷條件為真以后,代碼可以并發(fā)的切換到其他線程。
- usleep 這個模擬漫長業(yè)務(wù)的過程,在這個漫長的業(yè)務(wù)過程中,可能有很多個線程會進(jìn)入該代碼段。
- -- ticket 操作本身就不是一個原子操作,轉(zhuǎn)換成匯編有三條匯編指令。
- 當(dāng)ticket的值已經(jīng)等于1時,由于某一個usleep會有較長時間的等待,此時又會有幾個線程進(jìn)入,if語句內(nèi)部,所以這幾個線程又會對ticket多次-- 操作。也就會出現(xiàn)ticket出現(xiàn)小于0的情況。
- 上述中,ticket就是臨界資源,整個if語句就是臨界區(qū)。
?取出ticket--部分的匯編代碼
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>
3.加鎖互斥鎖mutex
解決辦法:
- 代碼必須要有互斥行為:當(dāng)代碼進(jìn)入臨界區(qū)執(zhí)行時,不允許其他線程進(jìn)入該臨界區(qū)。
- 如果多個線程同時要求執(zhí)行臨界區(qū)的代碼,并且臨界區(qū)沒有線程在執(zhí)行,那么只能允許一個線程進(jìn)入該臨界區(qū)。
- 如果線程不在臨界區(qū)中執(zhí)行,那么該線程不能阻止其他線程進(jìn)入臨界區(qū)。
?要做到這三點(diǎn),本質(zhì)上就是需要一把鎖。Linux上提供的這把鎖叫互斥量。
創(chuàng)建鎖:
pthread_mutex_t mutex;
初始化鎖:
?初始化互斥量有兩種方法:
方法1,靜態(tài)分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2,動態(tài)分配:
int pthread_mutex_init(pthread_mutex_t *restrict_mutex, const pthread_mutexattr_t *restrict
attr);
參數(shù):
- mutex:要初始化的互斥量
- attr:NULL
銷毀鎖:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要銷毀。
- 不要銷毀一個已經(jīng)加鎖的互斥量。
- 已經(jīng)銷毀的互斥量,要確保后面不會有線程再嘗試加鎖。
加鎖與解鎖:
int pthread_mutex_lock(pthread_mutex_t *mutex);//加鎖
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解鎖
返回值:成功返回0,失敗返回錯誤號。
調(diào)用 pthread_ lock 時,可能會遇到以下情況:
- 該鎖沒被其他線程持有,直接申請成功該鎖并且返回。
- 該鎖已經(jīng)被其他線程所持有,或者存在其他線程同時申請鎖,但沒有競爭到鎖,那么pthread_ lock調(diào)用線程會陷入阻塞(執(zhí)行流被掛起),等待其他線程解鎖。
?測試代碼:
將上述的代碼對臨界區(qū)加鎖,使得臨界區(qū)一次只能進(jìn)入一個線程,每次只能有一個線程對臨界資源訪問,即每次都只能有一個線程對ticket--。
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;
#define NUM 4 // 線程數(shù)
int ticket = 1000; // 1000票
pthread_mutex_t mutex; // 鎖
void *RobTicket(void *args)
{
const char *name = static_cast<const char *>(args);
while (1)
{
pthread_mutex_lock(&mutex); // 加鎖
if (ticket > 0)
{
usleep(2000);
printf("%s-ticket,%d\n", name, ticket);
ticket--; // 四個線程同時對ticket--,直到ticket為0
pthread_mutex_unlock(&mutex); // 解鎖
}
else
{
pthread_mutex_unlock(&mutex); // 解鎖
break;
}
usleep(100);
}
}
int main()
{
pthread_mutex_init(&mutex, NULL); // 初始化鎖
pthread_t tid[NUM];
for (int i = 0; i < NUM; i++)
{
char *name = new char[50];
sprintf(name, "Thread-%d", i + 1);
pthread_create(tid + i, NULL, RobTicket, name);
}
// 線程等待
for (int i = 0; i < NUM; i++)
{
pthread_join(tid[i], NULL);
}
return 0;
}
測試結(jié)果:
?說明:
- 不會再出現(xiàn)ticket為負(fù)數(shù)的情況了。
- 我們可以明顯的發(fā)現(xiàn),代碼的打印速度變慢了,因為臨界區(qū)的代碼,包括像顯示器打印的語句都被串行化了。
?4.鎖的底層原理
經(jīng)過上面的例子,大家已經(jīng)意識到單純的 i++ 或者 ++i 都不是原子的,有可能會有數(shù)據(jù)一致性問題。
為了實現(xiàn)互斥鎖操作,大多數(shù)體系結(jié)構(gòu)都提供了swap或exchange指令,該指令的作用是把寄存器和內(nèi)存單元的數(shù)據(jù)相交換,由于只有一條指令,保證了原子性,即使是多處理器平臺,訪問內(nèi)存的 總線周期也有先后,一個處理器上的交換指令執(zhí)行時另一個處理器的交換指令只能等待總線周期。
?說明:
由于初始化pthread_mutex_init () 初始化會將mutex在內(nèi)存初始化為1,第一個線程申請鎖的時候,會將自己線程內(nèi)部的一個寄存器%al初始化為0,并且將寄存器%al的值與mutex內(nèi)存的數(shù)據(jù)交換,那么現(xiàn)在該申請鎖的線程的%al寄存器中就存儲的是1,mutex的內(nèi)存中存儲的就是0,再有經(jīng)過檢測,如果%al的值是大于0的return之后,繼續(xù)往后運(yùn)行。后續(xù)的線程再申請鎖的時候,exchange之后,他們的%al寄存器只能存儲的是0,所以會被掛起等待。
解鎖僅僅需要將mutex的內(nèi)存數(shù)據(jù)重新賦值為1,并且喚醒掛起等待的進(jìn)程。
5.鎖封裝
class Mutex//自己不維護(hù)鎖,由外部傳入
{
public:
Mutex(pthread_mutex_t *mutex)
: _mutex(mutex)
{
}
void lock()
{
pthread_mutex_lock(_mutex);
}
void unlock()
{
pthread_mutex_unlock(_mutex);
}
~Mutex()
{
}
private:
pthread_mutex_t *_mutex;
};
class MutexGuard
{
public:
MutexGuard(pthread_mutex_t *mutex)
: _mutex(mutex)
{
_mutex.lock();
}
~MutexGuard()
{
_mutex.unlock();
}
private:
Mutex _mutex;
};
?二.可重入VS線程安全
1.概念
- 線程安全:多個線程并發(fā)同一段代碼時,不會出現(xiàn)不同的結(jié)果。常見對全局變量或者靜態(tài)變量進(jìn)行操作,并且沒有鎖保護(hù)的情況下,會出現(xiàn)該問題。
- 重入:同一個函數(shù)被不同的執(zhí)行流調(diào)用,當(dāng)前一個流程還沒有執(zhí)行完,就有其他的執(zhí)行流再次進(jìn)入,我們稱之為重入。一個函數(shù)在重入的情況下,運(yùn)行結(jié)果不會出現(xiàn)任何不同或者任何問題,則該函數(shù)被稱為可重入函數(shù),否則,是不可重入函數(shù)。
2.常見的線程不安全的情況
- 不保護(hù)共享變量的函數(shù)。
- 函數(shù)狀態(tài)隨著被調(diào)用,狀態(tài)發(fā)生變化的函數(shù)。
- 返回指向靜態(tài)變量指針的函數(shù)。
- 調(diào)用線程不安全函數(shù)的函數(shù)。
3.常見的線程安全的情況?
- 每個線程對全局變量或者靜態(tài)變量只有讀取的權(quán)限,而沒有寫入的權(quán)限,一般來說這些線程是安全的類或者接口對于線程來說都是原子操作。
- 多個線程之間的切換不會導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性。
4.常見不可重入的情況?
- 調(diào)用了malloc/free函數(shù),因為malloc函數(shù)是用全局鏈表來管理堆的。
- 調(diào)用了標(biāo)準(zhǔn)I/O庫函數(shù),標(biāo)準(zhǔn)I/O庫的很多實現(xiàn)都以不可重入的方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu)。
- 可重入函數(shù)體內(nèi)使用了靜態(tài)的數(shù)據(jù)結(jié)構(gòu)。
5..常見可重入的情況
- 不使用全局變量或靜態(tài)變量。
- 不使用用malloc或者new開辟出的空間。
- 不調(diào)用不可重入函數(shù)。
- 不返回靜態(tài)或全局?jǐn)?shù)據(jù),所有數(shù)據(jù)都有函數(shù)的調(diào)用者提供。
- 使用本地數(shù)據(jù),或者通過制作全局?jǐn)?shù)據(jù)的本地拷貝來保護(hù)全局?jǐn)?shù)據(jù)。
6.可重入與線程安全聯(lián)系
- 可重入函數(shù)是線程安全函數(shù)的一種。
- 線程安全不一定是可重入的,而可重入函數(shù)則一定是線程安全的。
- 如果將對臨界資源的訪問加上鎖,則這個函數(shù)是線程安全的,但如果這個重入函數(shù)若鎖還未釋放則會產(chǎn)生死鎖,因此是不可重入的。?
?三.死鎖
死鎖是指在一組進(jìn)程中的各個進(jìn)程均占有不會釋放的資源,但因互相申請被其他進(jìn)程所站用不會釋放的資源而處于的一種永久等待狀態(tài)。
1.死鎖四個必要條件
- 互斥條件:一個資源每次只能被一個執(zhí)行流使用。
- 請求與保持條件:一個執(zhí)行流因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:一個執(zhí)行流已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪。
- 循環(huán)等待條件:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關(guān)系。
2.避免死鎖
- 破壞死鎖的四個必要條件
- 加鎖順序一致
- 避免鎖未釋放的場景
- 資源一次性分配
?3.避免死鎖算法
- 死鎖檢測算法(了解)
- 銀行家算法(了解)?
四.Linux線程同步
1.條件變量
- 當(dāng)一個線程互斥地訪問某個變量時,它可能發(fā)現(xiàn)在其它線程改變狀態(tài)之前,它什么也做不了。
- 例如一個線程訪問隊列時,發(fā)現(xiàn)隊列為空,它只能等待,只到其它線程將一個節(jié)點(diǎn)添加到隊列中。這種情況就需要用到條件變量。
2.同步概念與競態(tài)條件
- 同步:在保證數(shù)據(jù)安全的前提下,讓線程能夠按照某種特定的順序訪問臨界資源,從而有效避免饑餓問題,叫做同步。
- 競態(tài)條件:因為時序問題,而導(dǎo)致程序異常,我們稱之為競態(tài)條件。?
?3.條件變量函數(shù)
定義條件變量:
pthread_cond_t cond;
初始化條件變量:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
參數(shù):
- cond:要初始化的條件變量。
- attr:NULL。
銷毀條件變量:
int pthread_cond_destroy(pthread_cond_t *cond);
等待條件滿足:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
參數(shù):
- cond:要在這個條件變量上等待。
- mutex:互斥量,后面詳細(xì)解釋。
注意:
條件變量一般需要搭配互斥鎖來使用,當(dāng)線程滿足等待條件時,需要線程先釋放鎖,再去等待。否則帶著鎖再條件變量等待,會導(dǎo)致其他線程申請鎖失敗。
喚醒等待:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
- pthread_cond_broadcast:一次喚醒所有在cond上等待的線程。
- pthread_cond_signal:喚醒一個在cond上等待的線程。
4.代碼樣例
#include <iostream>
#include <queue>
#include <cstdlib>
#include <cstdio>
#include <ctime>
#include <unistd.h>
#include <pthread.h>
using namespace std;
queue<int> V;
pthread_mutex_t mutex = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
pthread_cond_t cond;
void *push_date(void *args)
{
const char *name = static_cast<const char *>(args);
while (1)
{
int date;
cin >> date; // 輸入數(shù)據(jù)
// 對臨界資源的訪問需要加鎖
pthread_mutex_lock(&mutex);
V.push(date);
// 當(dāng)隊列中有數(shù)據(jù)以后,需要喚醒get_date線程
pthread_cond_signal(&cond);
cout << name << ":push 一個數(shù)據(jù) :" << date << endl;
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
void *get_date(void *args)
{
const char *name = static_cast<const char *>(args);
while (1)
{
pthread_mutex_lock(&mutex);
//如果隊列為空
if (V.empty())
{
pthread_cond_wait(&cond, &mutex);
}
int date = V.front();
V.pop();
cout << name << ":我得到一個數(shù)據(jù):" << date << endl;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
srand(time(nullptr));
//
pthread_t tid_push, tid_get;
pthread_create(&tid_get, nullptr, get_date, (void *)"Thread_get");
pthread_create(&tid_get, nullptr, push_date, (void *)"Thread_push");
pthread_join(tid_get, NULL);
pthread_join(tid_push, NULL);
return 0;
}
?測試結(jié)果:
文章來源:http://www.zghlxwxcb.cn/news/detail-742736.html
說明:文章來源地址http://www.zghlxwxcb.cn/news/detail-742736.html
- 只有我們輸入之后,get_date線程才會去隊列中獲取數(shù)據(jù)。
- 說明,get_gate線程再隊列為空的時候是不會去拿數(shù)據(jù)的,使得我們的push線程先push數(shù)據(jù)的邏輯始終在get數(shù)據(jù)之前。這就是線程同步。
到了這里,關(guān)于Linux——多線程,互斥與同步的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!