Linux下線程的互斥與同步詳解

🤖個人主頁:晚風相伴-CSDN博客

💖如果覺得內容對你有幫助的話,還請給博主一鍵三連(點贊💜、收藏🧡、關注💚)吧

🙏如果內容有誤或者有寫的不好的地方的話,還望指出,謝謝!!!

讓我們共同進步

?

下一篇《生產者消費者模型》敬請期待

目錄

🔥線程間互斥的相關概念

💪互斥量的接口

初始化互斥量

銷毀互斥量

互斥量的加鎖與解鎖

🔥探究互斥量實現原理

可重入函數和線程安全?

兩者的概念區分?

常見的線程不安全和安全情況

可重入與線程安全的聯系與區別

?死鎖?

產生死鎖的四個必要條件

避免死鎖

🔥線程同步?

條件變量?

同步的概念與競態條件

🔥條件變量接口

初始化?

銷毀條件?

條件等待

喚醒等待

🔥解釋pthread_cond_wait中的互斥量


🔥線程間互斥的相關概念

  • 臨界資源:多線程執行流共享的資源就叫做臨界資源
  • 臨界區:每個線程內部,訪問臨界資源的代碼,就叫做臨界區
  • 互斥:任何時刻,互斥保證有且只有一個執行流進入臨界區,訪問臨界資源,通常對臨界資源其保護作用
  • 原子性:不會被任何調度機制打斷的操作,該操作只有兩種狀態,要么完成,要么未完成。

先來看看下面簡單實現的搶票的代碼

int tickets = 1000;void* getTickets(void* args){(void)args;while(true){if(tickets > 0){usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;}else{break;}}return nullptr;}int main(){pthread_t t1, t2, t3;pthread_create(&t1, nullptr, getTickets, nullptr);pthread_create(&t1, nullptr, getTickets, nullptr);pthread_create(&t1, nullptr, getTickets, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;}

結果演示

?

💪為什么結果會出現-1呢?

原因:首先要知道一個線程什么時候被調度,調度多長時間,完全是有計算機確定的,程序員決定不了。tickets在進行減減操作時,是分三步的

①讀取數據到CPU內的寄存器中

②CPU內部進行計算--

③將結果寫回內存中

為了方便敘述,這里給線程編個號

一號線程來了,由于時間片很短執行到第②步就被切走了,二號線程來了,它沒有被打斷,所以它執行完了這三步,并且這個線程的優先級比較高,一直執行tickets--操作,直到tickets減到1停止,在執行到第①步的時候被切走了,而一號線程回來了,繼續從它被打斷的地方繼續向后執行,也就是從第②步開始繼續向后執行,在寫回內存后,tickets已經減到了1,但是這個線程又把tickets修改為了999,并且這時它的時間片很長,所以這次又一直將tickets減到了1,由于判斷條件tickets不為0,所以tickets繼續減減操作,此時tickets減為了0,此時二號線程來了,將0讀入到寄存器中進行減減操作,所以結果出現了-1,這就導致了問題的出現。

?

?要解決上面的問題,就需要做到以下三點:

  1. 代碼必須要有互斥行為:當代碼進入臨界區執行時,不允許其它線程進入臨界區。
  2. 如果多個線程同時要求執行臨界區的代碼,并且臨界區沒有線程在執行,那么只能允許一個線程進入該臨界區。
  3. 如果線程不在臨界區中執行,那么該線程不能阻止其它線程進入臨界區。

要做到以上三點,就需要一把互斥鎖,將臨界區資源鎖住,沒有拿到鑰匙的線程就不能訪問臨界區資源,這就能做到保護了臨界區資源。Linux上提供的這把互斥鎖叫互斥量。

?

💪互斥量的接口

初始化互斥量

有兩種方式初始化互斥量

方法一:全局初始化分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法二:局部初始化分配

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

參數:

  • mutex:要初始化的互斥量
  • attr:nullptr

返回值:成功返回0,失敗返回錯誤碼

銷毀互斥量

?int pthread_mutex_destroy(pthread_mutex_t *mutex);

參數:

  • mutex:要銷毀的互斥量

返回值:成功返回0,失敗返回錯誤碼

?銷毀互斥量時需要注意

  • 使用全局初始化的互斥量不需要銷毀
  • 不要銷毀一個已經加鎖的互斥量
  • 已經銷毀的互斥量要確保后面的代碼中不再有加鎖的操作

互斥量的加鎖與解鎖

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失敗返回錯誤碼

?調用pthread_mutex_lock加鎖時,可能會遇到以下情況:

  • 互斥量還沒被加鎖,處于未鎖定狀態,那么調用該函數會將互斥量加鎖鎖定。
  • 在調用該函數之前,其它線程已經申請了鎖,鎖定了該互斥量,或者存在其它線程同時競爭式的申請互斥量,但沒有競爭到互斥量,那么調用pthread_mutex_lock就會被阻塞,等待會吃兩解鎖。

所以將上面的搶票代碼修改如下:

int tickets = 1000; // 臨界資源class ThreadData
{
public:ThreadData(string &name, pthread_mutex_t *pmtx) : _tname(name), _pmtx(pmtx){}public:string _tname;pthread_mutex_t *_pmtx;
};void *getTickets(void *args)
{ThreadData* td = (ThreadData*)args;while (true){int n = pthread_mutex_lock(td->_pmtx); // 加鎖保護臨界區資源assert(n == 0);if (tickets > 0){usleep(1000);printf("%s : %d\n", td->_tname.c_str(), tickets);cout << td->_tname << " : " << tickets << endl;tickets--;n = pthread_mutex_unlock(td->_pmtx);assert(n == 0);}else{n = pthread_mutex_unlock(td->_pmtx);assert(n == 0);break;}// 處理后續的動作cout << "恭喜,搶票成功" << endl;usleep(1000);}return nullptr;
}#define THREAD_NUM 5int main()
{pthread_mutex_t mtx;pthread_mutex_init(&mtx, nullptr); // 局部定義的鎖進行初始化的形式pthread_t tid[THREAD_NUM];for (int i = 0; i < THREAD_NUM; i++){string name = "thread ";name += to_string(i + 1);ThreadData *td = new ThreadData(name, &mtx);pthread_create(tid + i, nullptr, getTickets, (void *)td);}for (int i = 0; i < THREAD_NUM; i++){pthread_join(tid[i], nullptr);}pthread_mutex_destroy(&mtx); // 最后將鎖釋放掉return 0;
}

?結果演示:

?

🔥探究互斥量實現原理

加鎖的目的是保證操作的原子性。?從匯編的角度來看,如果只有一條匯編語句,我們就認為該匯編語句的執行是原子的,?在匯編中給我們提供了swap或者exchange指令,該指令的作用是將內存中的數據與CPU內寄存器中的數據(CPU內寄存器中的數據也叫做執行流的上下文,寄存器的空間是被所有執行流鎖共享的,但是里面的數據是被某一個執行流私有的)進行交換,由于只有一條指令,所以可以保證其原子性。

?

?

解鎖時會把互斥量變為1。

可重入函數和線程安全?

兩者的概念區分?

線程安全:多個線程并發執行同一段代碼時,不會出現不同的結果。
重入:同一個函數被不同的執行流調用,當前一個執行流還沒有執行完,就有其它的執行流再次進入該函數,我們稱這種情況是重入。一個函數在重入的情況下,運行結果不會出現任何不同或者任何問題,則該函數被稱為可重入函數,否則稱為不可重入函數。

常見的線程不安全和安全情況

不安全情況:

  • 不保護共享變量的函數
  • 函數狀態隨著被調用,狀態發生變化的函數
  • 返回指向靜態變量指針的函數
  • 調用線程不安全函數的函數

?安全情況:

  • ?每個線程對全局變量或者靜態變量只有讀取權限,而沒有寫入權限,一般來說這些線程是安全的。
  • 類或者接口對于線程來說都是原子操作的
  • 多個線程之間的切換不會導致該接口的執行結果存在二義性

可重入與線程安全的聯系與區別

聯系:

  • 函數是可重入的,那就是線程安全的。
  • 函數是不可重入的,那就不能由多個線程使用,有可能引發線程安全問題。
  • 如果一個函數中有全局變量,那么這個函數既不是線程安全也不是可重入的。

區別:

  • 可重入函數是線程安全函數的一種
  • 線程安全不一定是可重入的,而可重入函數則一定是線程安全的
  • 如果對臨界資源的訪問加上鎖,則這個函數是線程安全的,但如果這個可重入函數的鎖還未釋放則會產生死鎖,因此是不可重入的。

?死鎖?

死鎖是指子在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其它進程所占用不會釋放的資源而處于的一種永久等待狀態。

產生死鎖的四個必要條件

  • 互斥條件:一個資源每次只能被一個執行流使用
  • 請求與保持條件:一個執行流因請求支援而阻塞時,對已獲得的資源保持不放
  • 不剝奪條件:一個執行流已獲得的資源,在未使用完之前,不能被強行剝奪
  • 循環等待條件: 若干執行流之間形成一種頭尾相接的循環等待資源的關系

避免死鎖

  • 破壞死鎖的四個必要條件
  • 加鎖順序一致
  • 避免鎖未釋放的場景
  • 資源一次性分配
  • 對死鎖檢測
  • 銀行家算法

🔥線程同步?

條件變量?

當我們申請臨界資源前,要先檢測臨界資源是否存在,做檢測的本質也是在訪問臨界資源,所以對臨界資源的檢測一定是要在加鎖和解鎖之間的。例如一個線程訪問隊列時,發現隊列為空,那么它只能等待,直到其它線程將一個節點添加到隊列中,在檢測隊列是否為空時,如果該線程一直輪詢檢測,那么勢必要頻繁的申請鎖和釋放鎖,這樣太浪費資源了,那么這種情況就需要用到條件變量了。

因此條件變量可以讓線程不在頻繁的自己檢測了,當第一次檢測到條件不滿足時就掛起等待,當條件滿足時,再通知該線程,讓它來申請資源和訪問。

同步的概念與競態條件

同步:在保證數據安全的前提下,讓線程能夠按照某種特定的順序訪問臨界資源,從而有效的解決了訪問臨界資源的合理性問題。

競態條件:因為時序問題,而導致程序異常,我們稱之為競態條件。

🔥條件變量接口

初始化?

和互斥量那里一樣分為全局初始化和局部初始化

局部初始化?

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

參數

  • cond:要初始化的條件變量
  • attr:設置為nullptr即可

返回值:成功返回0,失敗返回錯誤碼

全局初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;?

銷毀條件?

int pthread_cond_destroy(pthread_cond_t *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 <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;#define NUM 4
typedef void (*func_t)(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond);
volatile bool quit = false;class ThreadData
{
public:ThreadData(string &name, func_t func, pthread_mutex_t *pmtx, pthread_cond_t *pcond): _name(name), _func(func), _pmtx(pmtx), _pcond(pcond){}public:string _name;func_t _func;pthread_mutex_t *_pmtx;pthread_cond_t *_pcond;
};void func1(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx);//線程等待cout << name << " running... -- 1" << endl;// sleep(1);pthread_mutex_unlock(pmtx);}
}void func2(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx);//線程等待cout << name << " running... -- 2" << endl;// sleep(1);pthread_mutex_unlock(pmtx);}
}void func3(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx);//線程等待cout << name << " running... -- 3" << endl;// sleep(1);pthread_mutex_unlock(pmtx);}
}void func4(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx);//線程等待cout << name << " running... -- 4" << endl;// sleep(1);pthread_mutex_unlock(pmtx);}
}void* Entry(void* args)
{ThreadData* tmp = (ThreadData*)args;tmp->_func(tmp->_name, tmp->_pmtx, tmp->_pcond);delete tmp;return nullptr;
}int main()
{pthread_mutex_t mtx;pthread_cond_t cond;pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);pthread_t tid[NUM];func_t funcs[NUM] = {func1, func2, func3, func4};for (int i = 0; i < NUM; i++){string name = "thread ";name += to_string(i + 1);ThreadData* td = new ThreadData(name, funcs[i], &mtx, &cond);pthread_create(tid + i, nullptr, Entry, (void*)td);}int cnt = 10;while(cnt){cout << "resume thread run code..." << cnt-- << endl;pthread_cond_signal(&cond);// pthread_cond_broadcast(&cond);sleep(1);}cout << "ctrl done" << endl;quit = true;pthread_cond_broadcast(&cond);for(int i = 0; i < NUM; i++){pthread_join(tid[i], nullptr);cout << "pthread: " << tid[i] << " quit" << endl; }pthread_mutex_destroy(&mtx);pthread_cond_destroy(&cond);return 0;
}

結果演示

??

按照一定的順序執行。

🔥解釋pthread_cond_wait中的互斥量

條件等待是線程間同步的一種手段,如果只有一個線程,條件不滿足,一直等下去也都不會滿足,所以必須還要有一個線程通過某些操作來改變共享變量,使得不滿足的條件變得滿足,并且友好的通知在條件變量上等待的線程。但是條件不會無緣無故的滿足,這必然會牽扯到共享數據的改變。共享數據屬于臨界資源,因此一定要用互斥鎖來保護,沒有互斥鎖的保護就無法安全的獲取和修改共享數據了。

?

按照上面的說法,我們轉換成代碼,必須先上鎖,檢測到條件不滿足時,pthread_cond_wait會解鎖,然后在條件變量上等待,直到條件滿足時,pthread_cond_wait又會重新加鎖。

進入pthread_cond_wait函數后,會去檢測條件是否滿足,如果不滿足就把互斥量變為1(解鎖),直到條件滿足后(pthread_cond_wait返回)將互斥量恢復成原樣。

條件變量的規范使用如下

//等待條件代碼
pthread_mutex_lock(&mtx);
while(條件檢測)pthread_cond_wait(&cond, &mtx);
//修改條件
pthread_mutex_unlock(&mtx);//條件滿足,喚醒線程代碼
pthread_mutex_lock(&mtx);
//設置條件滿足
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);

??

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/24585.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/24585.shtml
英文地址,請注明出處:http://en.pswp.cn/web/24585.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

android:text 總為大寫字母的原因

當設置某個 Button 的 text 為英文時&#xff0c;界面上顯示的是該英文的大寫形式&#xff08;uppercase&#xff09;。例如&#xff1a; <Buttonandroid:id"id/btn"android:layout_width"wrap_content"android:layout_height"wrap_content"…

centos7 安裝 mysql5.7 LTS

centos7 安裝 mysql5.7 LTS 參考&#xff1a; https://blog.csdn.net/EB_NUM/article/details/105425622 可以在運行安裝程序之前導入密鑰&#xff1a; sudo rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022第一步、下載MySQL 安裝包&#xff1a; sudo wget h…

Python 中的內存管理機制

Python 的內存管理機制主要由兩個部分組成&#xff1a;垃圾回收機制和引用計數。 垃圾回收機制主要負責檢測和回收不再被使用的內存。Python 使用的是自動垃圾回收機制&#xff0c;也就是說程序員不需要手動釋放內存。Python 的垃圾回收機制采用了引用計數的方法來追蹤和回收不…

植物大戰僵尸雜交版破解C++實現

文章目錄 前言準備工作&#xff1a;基地址與偏移UI界面設計和綁定項目模板總覽圖生成與實現信號處理1、陽光值更新:BTN12、三種錢幣值更新:BTN2-BTN43、冷卻刷新:BTN54、鎖定陽光&#xff1a;check15、無冷卻&#xff1a;check26、OnTimer&#xff08;&#xff09;和OnClose&am…

git合并多個項目并保留提交版本記錄

目錄 一、場景 二、合并步驟 1.本地新建 all 目錄&#xff0c;并初始化 2.在 all 中添加 a&#xff0c;b&#xff0c;c 的遠程分支 3.驗證是否添加成功 4.在 all 目錄下&#xff0c;獲取 a, b,c 的 master 分支數據 5.合并項目并移動到子目錄中 6.推送 all 的 master 分支…

二開版微交易系統

下載地址&#xff1a;二開版微交易系統

集成學習概述

概述 集成學習(Ensemble learning)就是將多個機器學習模型組合起來&#xff0c;共同工作以達到優化算法的目的。具體來講&#xff0c;集成學習可以通過多個學習器相結合&#xff0c;來獲得比單一學習器更優越的泛化性能。集成學習的一般步驟為&#xff1a;1.生產一組“個體學習…

實戰 | YOLOv10 自定義數據集訓練實現車牌檢測 (數據集+訓練+預測 保姆級教程)

導讀 本文主要介紹如何使用YOLOv10在自定義數據集訓練實現車牌檢測 (數據集訓練預測 保姆級教程)。 YOLOv10簡介 YOLOv10是清華大學研究人員在Ultralytics Python包的基礎上&#xff0c;引入了一種新的實時目標檢測方法&#xff0c;解決了YOLO以前版本在后處理和模型架構方面…

規范系統運維:系統性能監控與優化的重要性與實踐

在當今這個高度信息化的時代&#xff0c;企業的IT系統運維工作顯得尤為關鍵。其中&#xff0c;系統性能監控和優化是運維工作中不可或缺的一環。本文旨在探討規范系統運維中系統性能監控與優化的重要性&#xff0c;并分享一些實踐經驗和策略。 一、系統性能監控與優化的重要性…

RAGFlow 學習筆記

RAGFlow 學習筆記 0. 引言1. RAGFlow 支持的文檔格式2. 嵌入模型選擇后不再允許改變3. 干預文件解析?4. RAGFlow 與其他 RAG 產品有何不同&#xff1f; ?5. RAGFlow 支持哪些語言&#xff1f; ?6. 哪些嵌入模型可以本地部署&#xff1f; ?7. 為什么RAGFlow解析文檔的時間比…

自動化裝箱封箱解決方案:深度探討其優勢及故障處理技巧

在當今這個快節奏、高效率的時代&#xff0c;自動化裝箱封箱解決方案以其獨特的優勢&#xff0c;正逐漸成為物流、倉儲等行業的新寵。它不僅能大幅提升作業效率&#xff0c;還能顯著降低人工成本&#xff0c;減少人為錯誤。星派將深度探討自動化裝箱封箱技術的顯著優勢&#xf…

【Vue】練習-mutations的減法功能

文章目錄 一、需求二、完整代碼 一、需求 步驟 二、完整代碼 Son1.vue <template><div class"box"><h2>Son1 子組件</h2>從vuex中獲取的值: <label>{{ $store.state.count }}</label><br><button click"handleA…

C# 界面控件中英切換

編程軟件:VS 2015 需求:界面有兩個按鈕&#xff0c;點擊可以將界面上所有控件進行不同語言的切換。 一共兩種方案&#xff0c;個人認為第二種方案使用范圍更廣&#xff08;這里以中英文切換為例&#xff09;。 方案一:如圖所示&#xff0c;建立兩個資源文件 將所需控件的中英…

海思SS928(SD3403)部署YOLOv5-YOLOv7步驟詳解

1. YOLO模型資料 本文檔內容以yolov5-7.0工程、yolov5s模型為例。 a. 模型結構 詳細的模型結構可以利用netron工具打開.pt或.onnx模型查看。 b. 模型參數即驗證結果 其中,YOLOv5n、YOLOv5s、YOLOv5m、YOLOv5l、YOLOv5x為五種類型的預訓練模型,其包含的檢測類別相…

利用Pandas進行數據清洗與過濾:Python實戰指南

利用Pandas進行數據清洗與過濾&#xff1a;Python實戰指南 作為一個Python愛好者和數據分析從業者&#xff0c;我一直在探索如何利用Python來更高效地處理和分析數據。Python語言以其簡單易學、功能強大的特點&#xff0c;成為了數據分析領域的寵兒。本文將分享一些實用的Pyth…

2024 cicsn ezbuf

文章目錄 參考protobuf逆向學習復原結構思路exp 參考 https://www.y4ng.cn/posts/pwn/protobuf/#ciscn-2024-ezbuf protobuf 當時壓根不知道用了protobuf這個玩意&#xff0c;提取工具也沒提取出來&#xff0c;還是做題做太少了&#xff0c;很多關鍵性的結構都沒看出來是pro…

android 異屏同顯---學習筆記

實現 Android 異屏同顯(多個屏幕顯示同樣的畫面)可以通過多種方法來完成,具體實現方式會根據你的需求和設備的支持情況有所不同。以下是幾種常見的方法: 方法 1:使用 Cast SDK 如果你想要將內容投屏到智能電視或其他支持 Cast 的設備上,可以使用 Google Cast SDK。 主…

Unity 集成 FMOD 音頻管理插件 2.02

Unity 集成 FMOD 音頻管理插件 2.02 3. 集成教程&#xff1a;3.1 設置Unity項目3.2 設置FMOD項目3.3 設置 FMOD for Unity3.4 添加聲音&#xff1a;卡丁車引擎3.5 添加聲音&#xff1a;氛圍3.6 添加聲音&#xff1a;音樂3.7 刪除現有音頻3.8 下一步 10. 腳本 API 參考10.1 基礎…

Java鎖的四種狀態(無鎖、偏向級鎖、輕量級鎖、重量級鎖)

介紹 首先&#xff0c;我們需要明確一點&#xff1a;偏向級鎖、輕量級鎖、重量級鎖只針對synchronized 鎖的狀態總共有四種&#xff0c;級別由低到高依次為&#xff1a;無鎖、偏向鎖、輕量級鎖、重量級鎖。 這四種鎖狀態分別代表什么&#xff0c;為什么會有鎖升級&#xff…

在UI界面中實現3d人物展示

簡要原理(設置雙攝像機): 為需要展示的3D人物單獨設置一個攝像機(只設置為渲染人物層級),主要攝像機的方向與人物方向一致,但攝像機需要需要旋轉180,設置的角度自行進行微調創建一個Render Texture類型的組件用于存儲攝像機渲染的內容UI上設置需要展示的圖片區域,圖片…