Linux線程同步與互斥(上)

目錄

前言

1.互斥

1.先來見一種現象(數據不一致問題)

2.如何解決上述問題

3.理解為什么數據會不一致&&認識加鎖的接口

4.理解鎖

5.鎖的封裝


前言

? 在前面對線程的概念和控制的學習過程中,我們知道了線程是共享地址空間的,也就是會共享大部分資源,那么這個時候就會產生新的問題——并發訪問,最直觀的感受就是每次運行得出的結果值大概率不一致,這種執行結果不一致的現象是非常致命,因為它具有隨機性,即結果可能是對的,也可能是錯的,無法可靠的完成任務

image-20250615172706982

? 為了解決這一問題,我們要引入新的解決方案——同步和互斥,我們先來講互斥!

1.互斥

image-20250615172818085

1.先來見一種現象(數據不一致問題)

? ?部分情況,線程使?的數據都是局部變量,變量的地址空間在線程棧空間內,這種情況,變量歸屬單個線程,其他線程?法獲得這種變量。

? 但有時候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過數據的共享,完成線程之間的交互。

? 多個線程并發的操作共享變量,會帶來?些問題,比如說下面的一段模擬搶票的實驗代碼

// 操作共享變量會有問題的售票系統代碼
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
?
int ticket = 100;
?
void *route(void *arg)
{char *id = (char *)arg;while (1){if (ticket > 0) // 1.判斷{usleep(1000); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 模擬搶票化的時間printf("%s sells ticket:%d\n", id, ticket); // 2.模擬搶到了票ticket--; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 3.票數--}else{break;}}return nullptr;
}
?
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
?pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

image-20250615183136247

可以看到結果都把票干到負數了,這在現實中可是一件很糟糕的事情,比如說高鐵明明只有200個座位,卻有201的人搶到了票,這個人是沒有位置的,說明多個線程并發的操作共享變量,會帶來?些問題

2.如何解決上述問題

上面的代碼中

臨界區:

while (1){if (ticket > 0) // 1.判斷{usleep(1000); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 模擬搶票化的時間printf("%s sells ticket:%d\n", id, ticket); // 2.模擬搶到了票ticket--; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 3.票數--}else{break;}}

共享資源是:int ticket =1000;

其他代碼都屬于非臨界區

我們要想辦法保護臨界區:通過在臨界區中前后加鎖可以保護起來!

#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
?
int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 對鎖進行初始化
?
void *route(void *arg)
{char *id = (char *)arg;while (1){pthread_mutex_lock(&lock);if (ticket > 0) // 1.判斷{usleep(1000); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 模擬搶票化的時間printf("%s sells ticket:%d\n", id, ticket); // 2.模擬搶到了票ticket--; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 3.票數--pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}return nullptr;
}
?
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
?pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

image-20250615184551620

可以從結果看到,此時就不會出現票數為負的情況了,順利解決數據不一致的問題

3.理解為什么數據會不一致&&認識加鎖的接口

首先我們需要知道的是ticket--不是原子性的操作,它會被匯編代碼轉換成三條指令

? load :將共享變量ticket從內存加載到寄存器中

? update : 更新寄存器??的值,執?-1操作

? store :將新值,從寄存器寫回共享變量ticket的內存地址

比如:

0xFF00 載入 ebx ticket
0xFF02 減少 ebx 1
0xFF04 寫回 0x1111 ebx

假設我們有A、B兩線程,ticket初始是100,在cpu調度A線程執行到0xFF04時要發生線程切換,此時需要保存A的上下文數據:ebx(ticket)為99,cpu的pc指針保存0xFF04地址,然后cpu開始調度B線程,B線程運氣很好,在循環執行讓ticket減到1之后剛好才要被切換,保存上下文之后cpu又重新調度A,此時pc指針保存的0xFF04地址是要執行寫回內存的指令,那么這個時候的ticket又回到了99,這就發生了數據不一致問題,也說明了ticket--不是原子性的操作

image-20250615212831730

[^] ?我們暫時這么去理解原子性:一條匯編就是原子的?

我們上面的票數減到負數其實主要的問題不是出在ticket--這個操作,而是出戰if條件判斷ticket>0這一操作上,對于ticket值是否大于0做判斷也是一種計算(邏輯計算,得到的是布爾值),執行時先載入cpu,再判斷;那么此時如果有3個線程,ticket此時為1,都完成1的載入后被切走了(因為加了休眠的時間,導致線程沒來及做--操作就讓下一個線程進來了),后面按順序喚醒線程時時并行判斷都是1就允許進入了,三個線程此時串行載入ticket,執行ticket--然后再寫回內存使得ticket此時從1->0->-1->-2就變成-2了

上面的問題告訴了我們:全局資源沒有加保護,可能會有并發問題——線程安全問題,同時要形成上面的問題需要在多線程中,制造更多的并發、更多的切換,切換的時間點:1.時間片到了 2.阻塞式IO 3.sleep等等...;選擇新的線程時間點:從內核態返回用戶態的時候,進行檢查

要解決以上問題,需要做到三點:

? 代碼必須要有互斥?為:當代碼進?臨界區執?時,不允許其他線程進?該臨界區。

? 如果多個線程同時要求執?臨界區的代碼,并且臨界區沒有線程在執?,那么只能允許?個線程進?該臨界區。

? 如果線程不在臨界區中執?,那么該線程不能阻?其他線程進?臨界區

要做到這三點,本質上就是需要?把鎖 ——pthread_mutex_t(互斥鎖/互斥量)

屏幕截圖 2025-06-16 165944

[^] ?pthread_mutex_init的第二個參數為鎖屬性,我們不用管設為nullptr就行?

加鎖規則:盡量加鎖的范圍粒度要比較細,盡可能不要包含太多的非臨界區代碼

image-20250616161349990

對臨界區進行保護本質其實就是用鎖來對臨界區進行保護

問題1:如果有線程不遵守我們的規則,那就是一個bug,所有線程必須遵守!!

問題2:枷鎖之后,在臨界區內部允許線程切換嗎?切換了會怎么樣?

答:允許切換,但是不會怎么樣,因為我當前線程并沒有釋放鎖,該線程持有鎖被切換,

其他線程也必須等我被切換回來執行完代碼、釋放鎖了才能展開申請鎖的競爭,進而

進入臨界區(當然這樣就會導致多線程執行代碼的速度變慢)

image-20250616165319481

加鎖和解鎖的本質就是把整個代碼塊進行原子化,讓其他無法中斷該線程

4.理解鎖

經過上?的例?,?家已經意識到單純的 i++或者 ++i都不是原?的,有可能會有數據?致性問題

鎖的原理:

  1. 硬件級實現:關閉時鐘中斷

  2. 軟件級實現:

    為了實現互斥鎖操作,大多數體系結構都提供了swap或exchange指令(只有一條指令保證原子性),該指令的作用是把寄存器和內存單元的數據相交換

    下面是一段鎖在匯編的偽代碼:

    image-20250616193123421

image-20250616191606705

5.鎖的封裝

其實在c++中用鎖很簡單,我們只需要包含#include<mutex.h>頭文件,然后定義一個鎖被封裝好的mutex類的對象,然后就可以用這個對象調用這個mutex類中的lock、unlock接口實現申請鎖和解鎖等操作啦(我們其實在c++階段是學過的)

image-20250616195740905

使用c++封裝的鎖來解決我們上面的搶票數據不一致問題代碼:

#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <mutex>
?
int ticket = 100;
// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 對鎖進行初始化
std::mutex lock;
?
void *route(void *arg)
{char *id = (char *)arg;while (1){// pthread_mutex_lock(&lock);lock.lock();if (ticket > 0) // 1.判斷{usleep(1000); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 模擬搶票化的時間printf("%s sells ticket:%d\n", id, ticket); // 2.模擬搶到了票ticket--; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 3.票數--// pthread_mutex_unlock(&lock);lock.unlock();}else{// pthread_mutex_unlock(&lock);lock.unlock();break;}}return nullptr;
}
?
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
?pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

我們當然也可以自己造個輪子,也跟著封裝一個我們自己的鎖

Mutex.hpp

#pragma once
#include <pthread.h>
#include <iostream>
?
namespace MutexModle
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr);}
?// 申請鎖void Lock(){// pthread_mutex_lock成功返回0,失敗返回錯誤碼int n = pthread_mutex_lock(&_mutex);if (n != 0){std::cerr << "申請鎖失敗" << std::endl;return;}}
?// 解鎖void Unlock(){int n = pthread_mutex_unlock(&_mutex);if (n != 0){std::cerr << "解鎖失敗" << std::endl;return;}}
?~Mutex(){pthread_mutex_destroy(&_mutex);}
?private:pthread_mutex_t _mutex;};
?// 實現RAII風格的互斥鎖class LockGuard{public:LockGuard(Mutex &mutex): _mutex(mutex){_mutex.Lock();}
?~LockGuard(){_mutex.Unlock();}
?private:Mutex &_mutex;};
}

TestMutex.cc

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "Mutex.hpp"
using namespace MutexModle;
?
int ticket = 100;
?
// 我們自己封裝的鎖類
Mutex lock;
?
void *route(void *arg)
{char *id = (char *)arg;while (1){// 申請鎖// lock.Lock();// 通過LockGuard類構造對象調用構造函數中的申請鎖代碼實現自動加鎖// 這就是RAII風格的互斥鎖的實現LockGuard guard(lock);
?if (ticket > 0) // 1.判斷{usleep(1000); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 模擬搶票化的時間printf("%s sells ticket:%d\n", id, ticket); // 2.模擬搶到了票ticket--; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 3.票數--// 解鎖// lock.Unlock();// 通過guard臨時對象出作用域會自動調用析構函數進行自動解鎖}else{// lock.Unlock();// 通過guard臨時對象出作用域會自動調用析構函數進行自動解鎖break;}}return nullptr;
}
?
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
?pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
?return 0;
}

結果當然也是顯而易見的成功解決數據不一致問題啦!

image-20250616204109527

我們上面其實實現了RAII風格(智能指針就是利用這個思想的)的互斥鎖

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

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

相關文章

Codeforces Global Round 27

ABC 略D將每個數拆成x*2的整數次冪&#xff0c;一個直接的想法是盡量把2的整數次冪給大的數。那么所有乘上2的整數次冪的數構成的序列單調遞減&#xff0c;反證法&#xff0c;如果序列中存在i j 使得a[i]<a[j]&#xff0c;那么我們不如把給a[i]乘的2的冪給a[j]乘。#include …

深入 Go 底層原理(二):Channel 的實現剖析

1. 引言"Do not communicate by sharing memory; instead, share memory by communicating." (不要通過共享內存來通信&#xff0c;而應通過通信來共享內存。) 這是 Go 語言并發設計的核心哲學。而 channel 正是實現這一哲學的核心工具。Channel 為 Goroutine 之間的…

Golang 語言的編程技巧之類型

1、介紹Golang 語言是一門靜態類型的編程語言&#xff0c;我們在編寫代碼時&#xff0c;為了提升代碼的靈活性&#xff0c;有時會使用空接口類型&#xff0c;對于空接口類型的變量&#xff0c;一般會通過類型斷言判斷變量的類型&#xff0c;而且可能還會遇到遇到類型轉換的場景…

計數組合學7.11(RSK算法)

7.11 RSK算法 在對稱函數理論中&#xff0c;有一個非凡的組合對應關系&#xff0c;稱為RSK算法。&#xff08;關于縮寫RSK的含義以及其他名稱&#xff0c;請參閱本章末尾的注釋。&#xff09;這里我們僅介紹RSK算法的最基本性質&#xff0c;從而能夠給出舒爾函數一些基本性質的…

國產嵌入式調試器之光? RT-Trace 初體驗!

做過嵌入式開發的工程師肯定都知道有這么個玩意兒 —— J-Trace&#xff0c;與我們日常使用的普通調試器不同點在于&#xff0c;它在基本的下載/調試代碼之上還具有非常強大的代碼運行跟蹤能力&#xff0c;從而實現代碼覆蓋率的分析、指令回溯、CPU 資源監控等一系列強大的功能…

SLAM中的非線性優化-2D圖優化之零空間實戰(十六)

終于有時間更新實戰篇了&#xff0c;本節實戰幾乎包含了SLAM后端的所有技巧&#xff0c;其中包括&#xff1a;舒爾補/先驗Factor/魯棒核函數/FEJ/BA優化等滑動窗口法的相關技巧&#xff0c;其中構建2D輪式里程計預積分以及絕對位姿觀測的10幀滑動窗口&#xff0c;并邊緣化最老幀…

知識隨記-----Qt 實戰教程:使用 QNetworkAccessManager 發送 HTTP POST

文章目錄Qt 網絡編程&#xff1a;使用 QNetworkAccessManager 實現 HTTP POST 請求概要整體架構流程技術名詞解釋技術細節注意事項&#xff1a;Qt 網絡編程&#xff1a;使用 QNetworkAccessManager 實現 HTTP POST 請求 概要 本文介紹如何使用 Qt 框架的網絡模塊&#xff08;…

wordpress批量新建產品分類

1、下載安裝插件&#xff1a;bulk-category-import-export2、激活插件后&#xff0c;左側點擊插件下的導入&#xff0c;選擇product categories&#xff0c;點擊下一步3、這里可以選擇導入的分類列表文件&#xff0c;可以選擇分隔符&#xff0c;CSV文件默認為‘&#xff0c;’要…

CentOS 鏡像源配置與 EOL 后的應對策略

引言 本文將詳細介紹如何使用 阿里云開源鏡像站 配置 CentOS 的各類軟件源&#xff0c;包括基礎源、歷史歸檔源&#xff08;vault&#xff09;、ARM 架構源、Stream 版本以及調試信息源&#xff08;debuginfo&#xff09;&#xff0c;并重點講解在 CentOS 8 停止維護后&#x…

CTF實戰:用Sqlmap破解表單輸入型SQL注入題(輸入賬號密碼/usernamepassword)

目錄 引言 步驟1&#xff1a;用Burp Suite捕獲表單請求 步驟2&#xff1a;用Sqlmap獲取數據庫名稱 參數解釋&#xff1a; 輸出示例&#xff08;根據題目環境調整&#xff09;&#xff1a; 步驟3&#xff1a;獲取目標數據庫中的表名 參數解釋&#xff1a; 輸出示例&#…

質數時間(二分查找)

題目描述如果把一年之中的某個時間寫作 a 月 b 日 c 時 d 分 e 秒的形式&#xff0c;當這五個數都為質數時&#xff0c;我們把這樣的時間叫做質數時間&#xff0c;現已知起始時刻是 2022 年的 a 月 b 日 c 時 d 分 e 秒&#xff0c;終止時刻是 2022 年的 u 月 v 日 w 時 x 分 y…

Python訓練Day29

浙大疏錦行 類的裝飾器裝飾器思想的進一步理解&#xff1a;外部修改、動態類方法的定義&#xff1a;內部定義和外部定義

新手DBA實戰指南:如何使用gh-ost實現MySQL無鎖表結構變更

新手DBA實戰指南:如何使用gh-ost實現MySQL無鎖表結構變更 作為DBA,大表結構變更(DDL)一直是令人頭疼的問題。傳統的ALTER TABLE操作會鎖表,嚴重影響業務連續性;而常見的pt-online-schema-change工具雖然能實現在線變更,但依賴觸發器機制,在高并發場景下性能表現不佳。本…

OSPF綜合

一、實驗拓撲二、實驗需求1、R4為ISP&#xff0c;其上只配置IP地址&#xff1b;R4與其他所直連設備間均使用公有IP&#xff1b; 2、R3-R5、R6、R7為MGRE環境&#xff0c;R3為中心站點&#xff1b; 3、整個OSPF環境IP基于172.16.0.0/16劃分&#xff1b;除了R12有兩個環回&#x…

技術面試知識點詳解 - 從電路到編程的全棧面經

技術面試知識點詳解 - 從電路到編程的全棧面經 目錄 模擬電路基礎數字電路原理電源設計相關編程語言基礎數據庫與并發網絡協議基礎算法與數據結構 模擬電路基礎 1. 放大電路類型判斷 這是模擬電路面試的經典題目&#xff0c;通過電壓放大倍數判斷放大電路類型&#xff1a; …

LangGraph認知篇-Command函數

Command簡述 在 LangGraph 中&#xff0c;Command 是一個極具實用性的功能&#xff0c;它能夠將控制流&#xff08;邊&#xff09;和狀態更新&#xff08;節點&#xff09;巧妙地結合起來。這意味著開發者可以在同一個節點中&#xff0c;既執行狀態更新操作&#xff0c;又決定下…

【目標檢測】小樣本度量學習

小樣本度量學習&#xff08;Few-Shot Metric Learning&#xff09;通常用于分類任務?&#xff08;如圖像分類&#xff09;&#xff0c;但它也可以與目標檢測&#xff08;Object Detection&#xff09;結合&#xff0c;解決小樣本目標檢測&#xff08;Few-Shot Object Detectio…

cmd怎么取消關機命令

在 Windows 的命令提示符&#xff08;CMD&#xff09;中取消已計劃的關機操作&#xff0c;可以通過 shutdown 命令的 ?**-a**? 參數實現。以下是具體步驟&#xff1a;?操作方法??打開 CMD?按下 Win R 組合鍵&#xff0c;輸入 cmd 并回車&#xff0c;打開命令提示符窗口。…

網易云音樂硬剛騰訊系!起訴SM娛樂濫用市場支配地位

企查查APP顯示&#xff0c;近日&#xff0c;法院公開杭州樂讀科技有限公司、杭州網易云音樂科技有限公司起訴SM ENTERTAINMENT CO. 、卡斯夢&#xff08;上海&#xff09;文化傳播有限公司等開庭信息&#xff0c;案由涉及濫用市場支配地位糾紛。公告顯示&#xff0c;該案件計劃…

[css]切角

使用css實現一個切角的功能&#xff0c;有以下幾種方案&#xff1a; <div class"box"></div>方案一&#xff1a;linear-gradient linear-gradient配合backgroud-image可以實現背景漸變的效果。linear-gradient的漸變過渡區的占比是總的空間&#xff08;高…