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