Linux中的線程安全與線程同步詳解
一、線程安全
1、概念
我們這里通過理解重入與線程安全的關(guān)系來理解線程安全
線程安全即多個(gè)線程并發(fā)同一段代碼時(shí),不會(huì)出現(xiàn)不同的結(jié)果
重入即同一個(gè)函數(shù)被不同的執(zhí)行流調(diào)用,當(dāng)前一個(gè)流程還沒有執(zhí)行完,就有其他的執(zhí)行流再次進(jìn)入,一個(gè)函數(shù)在重入的情況下運(yùn)行結(jié)果不會(huì)出現(xiàn)任何問題,這樣的函數(shù)稱為可重入函數(shù),否則,就是不可重入函數(shù)
2、常見線程情況
常見線程不安全情況
- 不保護(hù)共享變量的函數(shù)
- 函數(shù)狀態(tài)隨著被調(diào)用,狀態(tài)發(fā)生變化的函數(shù)
- 返回指向靜態(tài)變量指針的函數(shù)
- 調(diào)用線程不安全函數(shù)的函數(shù)
常見線程安全情況
- 每個(gè)線程對(duì)全局變量或靜態(tài)變量只有讀權(quán)限沒有寫權(quán)限
- 類或接口對(duì)于線程來說是原子操作
- 多個(gè)線程之間的切換不會(huì)導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性
3、常見重入情況
常見不可重入情況
- 調(diào)用了malloc或new函數(shù),因?yàn)閙alloc函數(shù)是用全局鏈表來管理堆的
- 調(diào)用了標(biāo)準(zhǔn)IO庫(kù)函數(shù),標(biāo)準(zhǔn)IO庫(kù)中很多實(shí)現(xiàn)都以不可重入的方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu)
- 可重入函數(shù)體內(nèi)使用了靜態(tài)的數(shù)據(jù)結(jié)構(gòu)
常見可重入情況
- 不使用全局和靜態(tài)變量
- 不使用malloc或new出來的空間
- 不調(diào)用不可重入函數(shù)
- 不返回靜態(tài)或全局?jǐn)?shù)據(jù),所有數(shù)據(jù)都由函數(shù)的調(diào)用者提供
- 使用本地?cái)?shù)據(jù),或者通過制作全局?jǐn)?shù)據(jù)的本地拷貝來保護(hù)全局?jǐn)?shù)據(jù)
4、可重入與線程安全
聯(lián)系
- 函數(shù)可重入就代表著線程安全
- 函數(shù)不可重入,那就不能由多個(gè)線程使用,有可能引發(fā)線程安全問題
- 如果一個(gè)函數(shù)中有全局變量,這么這個(gè)函數(shù)既是不可重入的又不是線程安全的
區(qū)別
- 可重入函數(shù)是線程安全函數(shù)的一種
- 線程安全不一定是可重入的,但可重入的一定是線程安全的
- 如果將對(duì)臨界資源的訪問加上鎖,那么這個(gè)函數(shù)是線程安全的,但如果這個(gè)重入函數(shù)的鎖還沒釋放則會(huì)產(chǎn)生死鎖,因此是不可重入的
5、死鎖
(一)概念
死鎖是指在一組進(jìn)程或線程中的各個(gè)進(jìn)程或線程均占有不會(huì)釋放的資源,但因互相申請(qǐng)被其他進(jìn)程或線程所占用的不會(huì)釋放的資源而處于的一種永久等待的狀態(tài)
死鎖都是人為產(chǎn)生的,我們可以規(guī)避掉的
(二)死鎖的四個(gè)必要條件
- 互斥條件:一個(gè)資源只能被一個(gè)執(zhí)行流使用
- 請(qǐng)求與保持條件:一個(gè)執(zhí)行流因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放
- 不剝奪條件:一個(gè)執(zhí)行流已獲得的資源在未使用完之前。不能強(qiáng)行剝奪
- 循環(huán)等待條件:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關(guān)系
(三)避免死鎖的方法
- 當(dāng)有死鎖的時(shí)候,必然是滿足上面這四個(gè)條件的,但滿足上面四個(gè)條件不一定形成死鎖,我們只要破壞上面其中任何一條條件就可以避免死鎖
- 加鎖順序一致
- 避免鎖未釋放的場(chǎng)景
- 資源一次性分配
二、線程同步
1、概念
在純互斥的場(chǎng)景下,由于我們的鎖只有少量個(gè),多個(gè)線程同時(shí)競(jìng)爭(zhēng)鎖,但是得到鎖的只有一小部分線程,剩下的線程就會(huì)因?yàn)榈却?,產(chǎn)生 “線程饑餓” 問題,線程饑餓本質(zhì)上就是搶奪不到鎖的線程,即搶奪不到資源的線程在等待鎖的釋放,為了避免這里的饑餓的問題,我們就通過線程同步來在保證數(shù)據(jù)安全的前提下,讓線程按照順序訪問臨界資源
2、條件變量
(一)概念
當(dāng)一個(gè)線程互斥的訪問某個(gè)變量時(shí),它可能在其他線程改變狀態(tài)之前什么也做不了,比如一個(gè)線程訪問隊(duì)列時(shí),發(fā)現(xiàn)隊(duì)列為空,那么它只能等待,直到其他進(jìn)程將一個(gè)節(jié)點(diǎn)添加到隊(duì)列當(dāng)中,這個(gè)時(shí)候我們就可以利用條件變量來規(guī)避這種情況
(二)調(diào)用函數(shù)
(1)初始化條件變量
#include <pthread.h> int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
- 返回值:成功返回0,失敗返回非0錯(cuò)誤碼
cond
:指向要初始化的條件變量的指針,pthread_cond_t
是一個(gè)表示條件變量的數(shù)據(jù)類型attr
:指向條件變量屬性對(duì)象的指針,傳入NULL
表示使用默認(rèn)屬性
(2)銷毀條件變量
#include <pthread.h> int pthread_cond_destroy(pthread_cond_t *cond);
- 返回值:成功返回0,失敗返回非0錯(cuò)誤碼
cond
:指向要銷毀的條件變量的指針
(3)等待條件被滿足
#include <pthread.h> int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
- 返回值:成功返回0,失敗返回非0錯(cuò)誤碼
cond
:指向要操作的條件變量的指針,條件變量用于線程之間的等待和通知機(jī)制mutex
:指向互斥鎖的指針,互斥鎖用于保護(hù)共享資源,確保線程安全
調(diào)用該函數(shù)時(shí),線程會(huì)自動(dòng)釋放互斥鎖mutex,以便其他線程可以獲取鎖,當(dāng)收到信號(hào)被喚醒后,線程會(huì)重新嘗試獲取互斥鎖
(4)喚醒等待線程
#include <pthread.h> //喚醒一個(gè)等待線程 int pthread_cond_signal(pthread_cond_t *cond); //喚起所有等待線程 int pthread_cond_broadcast(pthread_cond_t *cond);
- 返回值:成功返回0,失敗返回非0錯(cuò)誤碼
cond
:指向要操作的條件變量的指針,條件變量是一種用于線程同步的機(jī)制,允許線程在某個(gè)條件不滿足時(shí)阻塞,直到其他線程通知該條件已經(jīng)滿足
如果一個(gè)線程執(zhí)行 pthread_cond_broadcast
,它會(huì)將所有等待該條件變量的線程全部喚醒,若執(zhí)行 pthread_cond_signal
,則只會(huì)喚醒至少一個(gè)等待該條件變量的線程,而非只喚醒當(dāng)前線程
(三)樣例
#include <iostream> #include <pthread.h> #include <vector> #include <unistd.h> using namespace std; #define NUM 4 int cnt = 0; //條件變量函數(shù)的用法幾乎與鎖函數(shù)的用法完全等同 //定義全局鎖和全局條件變量 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void *Count(void *args) { pthread_detach(pthread_self()); // 線程分離,跑完就不管了,不在乎它的返回值 // Linux是64位機(jī),指針是8字節(jié),uint是unsigned long long int uint64_t num = (uint64_t)args; cout << "Thread " << num << " is creat success" << endl; usleep(100000); while (true) { pthread_mutex_lock(&lock); //這里pthread_cond_wait要在臨界區(qū)的原因是: //因?yàn)?pthread_cond_wait 是讓線程去等待,等待的原因一定是臨界資源不就緒 //而臨界資源是否就緒,是通過判斷得來的,判斷也是訪問臨界資源,所以判斷必須在加鎖之后 pthread_cond_wait(&cond, &lock); //線程在此處進(jìn)入等待狀態(tài),等待條件變量 cond 發(fā)出信號(hào) cout << "Thread " << num << " is running... cnt: " << cnt << endl; cnt++; usleep(10000); pthread_mutex_unlock(&lock); } } int main() { for (uint64_t i = 1; i <= NUM; i++) { pthread_t tid; //這里的第四個(gè)參數(shù),如果想要與新線程共享這個(gè)參數(shù)的話,可以設(shè)為(void*)&i,進(jìn)行傳址調(diào)用 //我們這里要傳值調(diào)用,不能讓它用i pthread_create(&tid, nullptr, Count, (void *)i); usleep(1000); } //指定喚醒線程來訪問臨界資源 while (true) { sleep(1); pthread_cond_signal(&cond); // 喚醒一個(gè)線程 cout << "signal one thread..." << endl; } return 0; }
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Jexus 5.8.2正式發(fā)布! 為Asp.Net Core生產(chǎn)環(huán)境提供平臺(tái)支持
Jexus 5.8.2正式發(fā)布!Jexus支持ASP.NET、PHP為特色的集高安全性和高性能為一體的WEB服務(wù)器和反向代理服務(wù)器,感興趣的小伙伴們可以參考一下2017-06-06Xshell7遠(yuǎn)程連接失敗(connection failed)的問題解決
本文主要介紹了Xshell7遠(yuǎn)程連接失敗(connection failed)的問題解決,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08Linux 內(nèi)核通用鏈表學(xué)習(xí)小結(jié)
本篇文章主要介紹了Linux 內(nèi)核通用鏈表學(xué)習(xí)小結(jié),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-11-11Linux使用dd命令來復(fù)制和轉(zhuǎn)換數(shù)據(jù)的操作方法
Linux 中的 dd 命令是一個(gè)功能強(qiáng)大的數(shù)據(jù)復(fù)制和轉(zhuǎn)換實(shí)用程序,它以較低級(jí)別運(yùn)行,通常用于創(chuàng)建可啟動(dòng)的 USB 驅(qū)動(dòng)器、克隆磁盤和生成隨機(jī)數(shù)據(jù)等任務(wù),本文給大家介紹了Linux 如何使用dd命令來復(fù)制和轉(zhuǎn)換數(shù)據(jù),需要的朋友可以參考下2025-01-01CentOS7系統(tǒng)增加swap的操作方法實(shí)例
這篇文章主要給大家介紹了關(guān)于CentOS7系統(tǒng)增加swap的操作方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用CentOS7系統(tǒng)具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10解析linux或android添加文件系統(tǒng)的屬性接口的方法
這篇文章主要介紹了linux或android添加文件系統(tǒng)的屬性接口的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03