📚?博主的專欄
🐧?Linux???|?? 🖥??C++???|?? 📊?數據結構??|?💡C++ 算法?| 🌐?C 語言
上篇文章:?【Linux】線程:從原理到實戰,全面掌握多線程編程!-CSDN博客
下篇文章:? ?線程同步、條件變量、生產者消費者模型
目錄
線程ID
線程ID是是虛擬地址
庫內部承擔對線程的管理
庫如何管理線程的:先描述再組織。
Linux線程 = pthread庫中的線程的屬性集 +內核LWP(比率1:1)
clone創建的執行流默認是和主進程共享地址空間。
__thread?讓每個線程各自私一份的同一個名稱的變量
簡單封裝線程
準備三個文件:
實現線程控制的幾個方法
Start()
Stop()與Join()
簡單的線程封裝成品:
創建一批線程:對一批線程進行管理
線程互斥
搶票程序:
可重入VS線程安全
常見鎖概念
死鎖
死鎖四個必要條件
1.?什么是互斥鎖?
2. 互斥鎖的核心接口(C語言,pthread庫)
1.?初始化互斥鎖
(1)?靜態初始化
(2)?動態初始化
2.?加鎖與解鎖
(1)?阻塞加鎖
(2)?非阻塞加鎖
(3)?解鎖
3.?銷毀互斥鎖
3.所謂的對臨界資源進行保護,本質是對臨界區代碼進行保護
注意:
修改線程封裝:
從原理角度理解這個鎖:
從實現角度理解鎖:
加鎖(lock)邏輯
解鎖(unlock)邏輯
線程ID
線程ID是是虛擬地址
示例代碼:
#include <iostream> #include <string> #include <unistd.h> #include <pthread.h>void *threadrun(void *args) {std::string name = static_cast<const char *>(args); // 使用static_cast強轉類型,得到線程的名字while (true){ // 任何一個線程可以通過pthread_self獲取自己的線程idstd::cout << name << " " << "is running, tid: " << pthread_self() << std::endl;sleep(1);} }int main() {pthread_t tid;// 1.創建一個線程,1.取地址線程id 2.線程屬性設為nullptr 3.線程要執行的函數 4.線程的名字(參數強轉為void*)pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");std::cout << "new thread tid: " << tid << std::endl;// 2.線程一旦創建就需要join它pthread_join(tid, nullptr);return 0; }
給用戶提供的線程的id,不是內核中的LWP(輕量級進程),而是自己(pthread庫)維護的一個值。
?
庫內部承擔對線程的管理
將id值轉成16進制打印出:
std::string ToHex(pthread_t tid) {char id[128];snprintf(id, sizeof(id), "0x%lx", tid);return id; }void *threadrun(void *args) {std::string name = static_cast<const char *>(args); // 使用static_cast強轉類型,得到線程的名字while (true){ // 任何一個線程可以通過pthread_self獲取自己的線程idstd::string id = ToHex(pthread_self());std::cout << name << " " << "is running, tid: " << id << std::endl;sleep(1);} }int main() {pthread_t tid;// 1.創建一個線程,1.取地址線程id 2.線程屬性設為nullptr 3.線程要執行的函數 4.線程的名字(參數強轉為void*)pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");std::cout << "new thread tid: " << ToHex(tid) << std::endl;// 2.線程一旦創建就需要join它pthread_join(tid, nullptr);return 0; }
?
線程id實際上是一個地址。
動態庫在沒有被加載的時候在哪里?在磁盤中。庫是什么?文件
pthread庫本身是一個文件libpthread.so,而我寫的文件mythread也是一個文件在磁盤當中
多線程在啟動之前,也必須先是一個進程,再動態的創建線程。
而創建線程,前提是把庫加載到內存,映射到我進程的地址空間。
?
當線程動態庫已經加載到內存,庫中有pthread_create()方法,線程還不能夠執行這個方法。需要先將庫映射到堆棧之間的共享區當中,在共享區中構建了對應的動態庫的起始地址經過頁表再映射到內存的整個庫中,建立好了映射,未來想要訪問任意函數地址就通過頁表映射找到庫中方法,比如創建線程,就通過頁表的映射,找到了內存庫中創建線程的函數地址,在庫中將線程創建好。
?
庫如何管理線程的:先描述再組織。
在虛擬地址空間中:在動態庫里,創建一個線程的時候,庫會創建一個對應的線程控制塊tcb在內存當中,線程控制塊當中有struct pthread用于描述該線程的相關結構體字段屬性、以及線程棧(每一個新線程都有自己獨立的棧空間)。pthread_t tid就是每個線程控制塊的起始地址。因此只要有tid,就能訪問到這個線程的所有屬性。線程的所有屬性在庫里被維護(不代表在庫里開空間,空間還可以在堆上開,管理字段放庫里)
?
這里可以聯想到之前講到的知識點,在文件管理的時候,C語言的額FILE*(是C標準庫申請的),打開一個文件(fopen函數)會返回FILE*,那么FILE在哪里,在C標準庫里面。用地址訪問文件對象。
?
創建一個線程過后,在沒有pthread_join的時候,執行流已經關閉,線程已經退出return/pthread_exit(num),此時會將num放置struct pthread屬性當中,線程底層的內核LWP直接釋放,該線程的屬性一直被維護在庫里,直到join通過線程id也就是線程控制塊的地址,拿到線程退出的信息num。這就是為什么,不join會導致類似于僵尸進程的問題,因為在庫中還保留該線程的信息,沒有被釋放。
struct tcb內部包含數據塊:struct pthread other、char stack[N]等。每一個新線程棧實際上也是和主線程的棧一樣存在虛擬地址空間內部的,是用戶級空間,因此可以隨我們訪問。
pthread_t 到底是什么類型呢?取決于實現。對于Linux目前實現的NPTL實現而言,pthread_t類型的線程ID,本質就是一個進程地址空間上的一個地址。
Linux線程 = pthread庫中的線程的屬性集 +內核LWP(比率1:1)
如何保證一個新線程在執行程序時產生的臨時變量存在自己的棧空間內
Linux有LWP的系統調用
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ... /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
創建輕量級進程:clone(也是創建子進程fork()函數的底層實現)
讓創建的lwp去執行我設置的回調函數,所形成的臨時變量放置于我所指明的新線程棧空間內。?
fn?
子進程/線程啟動函數(類似線程入口函數)
返回值為子進程的退出狀態碼
st?ack
必須提供子進程獨立的棧空間地址
示例:
char stack[STACK_SIZE] = {0};
flags?
(關鍵控制位)
標志位 作用 CLONE_VM
共享虛擬內存空間(實現線程核心特性) CLONE_FS
共享文件系統屬性(根目錄、umask等) CLONE_FILES
共享文件描述符表 CLONE_SIGHAND
共享信號處理函數表 CLONE_THREAD
將新進程放入父進程的線程組 CLONE_SYSVSEM
共享System V信號量
?arg
傳遞給
fn
函數的參數
clone創建的執行流默認是和主進程共享地址空間。
因為,主線程和新線程共享地址空間,全局變量(存在全局區)對于多線程來說是是被共享的,因此主線程和新線程都能訪問到同一個全局變量。
示例:證明clone創建的默認執行流是和主進程共享地址空間的
#include <iostream> #include <string> #include<stdio.h> #include <unistd.h> #include <pthread.h>int gval = 100;//全局變量 std::string ToHex(pthread_t tid) {char id[128];snprintf(id, sizeof(id), "0x%lx", tid);return id; }void *threadrun(void *args) {std::string name = static_cast<const char *>(args); // 使用static_cast強轉類型,得到線程的名字while (true){ // 任何一個線程可以通過pthread_self獲取自己的線程idstd::string id = ToHex(pthread_self());std::cout << name << " " << "is running, tid: " << id <<",gval:"<< gval << ",&gval:" << &gval << std::endl;sleep(1);gval++;} }int main() {pthread_t tid;// 1.創建一個線程,1.取地址線程id 2.線程屬性設為nullptr 3.線程要執行的函數 4.線程的名字(參數強轉為void*)pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");while(true){ //主線程不對gval做修改std::cout << "main thread, gval:" << gval << ",&gval:" << &gval << std::endl;sleep(1);}std::cout << "new thread tid: " << ToHex(tid) << std::endl;// 2.線程一旦創建就需要join它pthread_join(tid, nullptr);return 0; }
主線程,新線程訪問到同一個全局變量?
__thread?讓每個線程各自私一份的同一個名稱的變量
Linux適用,只支持內置類型
簡單封裝線程
準備三個文件:
Thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>namespace ThreadModle
{// 線程要執行的方法typedef void (*func_t)(const std::string &name); // 函數指針類型class Thread{public:Thread(){}~Thread(){}void Start(){}void Stop(){}void Join(){}private:std::string _name; // 線程名pthread_t _tid; // 線程所對應的idbool _isrunning; // 線程此時是否正在運行func_t _func; // 線程要執行的回調函數};
}
Main.cc
#include<iostream>
#include"Thread.hpp"int main()
{Thread<int> t;//類模版創建一個線程對象t.Start();//啟動線程的方法t.Stop();//停止線程的方法t.join();//回收線程return 0;
}
Makefile
testthread:Main.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f testthread
實現線程控制的幾個方法
Start()
ThreadRoutine是類內的成員方法,這就意味著,這個方法默認帶了當前對象的this指針(類型是Thread*),而創建線程的函數要求所傳的該參數必須是一個返回值是void*,參數只有一個void*的函數指針。因此在類當中想要創建線程執行類的成員方法,是不可能的。
解決辦法:加static,static定義的類成員函數是沒有this指針的,這個成員函數就屬于類,不屬于對象了。?
帶來的問題:不能在ThreadRoutine再調用_func(),因為_func是私有的類內部成員屬性
解決辦法:在pthread_create函數傳參數時,將當前對象傳進ThreadRoutine,再強轉args成對應的對象。再寫一個成員函數Excute()用于調用回調函數,在ThreadRoutine直接用當前對象調用這個成員函數,就相當于調用回調函數,增加代碼可讀性,Excute()還能用來判斷我的線程是否已經開始running:_isrunning = true
namespace ThreadModle
{// 線程要執行的方法typedef void (*func_t)(const std::string &name); // 函數指針類型class Thread{public:void Excute() //調用回調函數的成員方法{_isrunning = true;_func(_name);}public:Thread(){}~Thread(){}static void *ThreadRoutine(void* args)//創建的新線程都會執行這個方法{//執行回調方法,每一個線程都能執行一系列的方法Thread* self = static_cast<Thread*>(args);//獲得了當前對象self->Excute();}bool Start(){ //創建線程成功就返回0int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);// ThreadRoutine線程的固定歷程if(n != 0) return false;return true;}void Stop(){}void Join(){}private:std::string _name; // 線程名pthread_t _tid; // 線程所對應的idbool _isrunning; // 線程此時是否正在運行func_t _func; // 線程要執行的回調函數};}
Stop()與Join()
首先,如果線程已經啟動了才能stop,因此要先判斷線程是否已經啟動。如果線程已經結束了才join,因此要先判斷線程是否已經結束。
void Stop(){if(_isrunning){pthread_cancel(_tid);//取消線程_isrunning = false; //設置狀態為false線程停止}}void Join(){pthread_join(_tid, nullptr);}
想要得到線程返回結果,可以修改回調函數的返回值為我想要的類型(返回結果),
typedef std::string (*func_t)(const std::string &name); // 函數指針類型
再在Thread類中封裝一個成員屬性result:
std::string result;
以及從Excute當中調用_func()的時候獲取返回結果:
void Excute() //調用回調函數的成員方法{_isrunning = true;_result = _func(_name);}
簡單的線程封裝成品:
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>namespace ThreadModle
{// 線程要執行的方法typedef void (*func_t)(const std::string &name); // 函數指針類型class Thread{public:void Excute() // 調用回調函數的成員方法{std::cout << _name << ",is running" << std::endl;_isrunning = true;_func(_name);_isrunning = false;}public:Thread(const std::string &name, func_t func) : _name(name), _func(func){std::cout << "create " << name << " done" << std::endl;}~Thread(){// Stop();// Join();}static void *ThreadRoutine(void *args) // 創建的新線程都會執行這個方法{// 執行回調方法,每一個線程都能執行一系列的方法Thread *self = static_cast<Thread *>(args); // 獲得了當前對象self->Excute();return nullptr;}bool Start(){ // 創建線程成功就返回0int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this); // ThreadRoutine線程的固定歷程if (n != 0)return false;return true;}std::string Status(){if(_isrunning) return "running";else return "sleep";}void Stop(){if (_isrunning){pthread_cancel(_tid); // 取消線程_isrunning = false; // 設置狀態為false線程停止std::cout << _name << " Stop" << std::endl;}}void Join(){pthread_join(_tid, nullptr);std::cout << _name << " Joined" << std::endl;}private:std::string _name; // 線程名pthread_t _tid; // 線程所對應的idbool _isrunning; // 線程此時是否正在運行func_t _func; // 線程要執行的回調函數// std::string _result;};}
Main.cc
void Print(const std::string &name){int cnt = 1;while (true){std::cout << name << "is running, cnt: " << cnt++ << std::endl;sleep(1);}}
運行結果:
?stop之后只剩一個線程:join后,都結束
?通過以上代碼,能夠感受線程在C++11里,實際上就是對原生線程的一種封裝
創建一批線程:對一批線程進行管理
管理原生線程,先描述,再組織(這里用數組下標就管理了線程)對vector的增刪查改
#include<iostream> #include"Thread.hpp" #include<vector> #include<unistd.h> using namespace ThreadModle; const int gnum = 10;void Print(const std::string &name) {int cnt = 1;while (true){std::cout << name << ",is running, cnt: " << cnt++ << std::endl;sleep(1);} } int main(){// 我在管理原生線程, 先描述,在組織// 構建線程對象std::vector<Thread> threads;for (int i = 0; i < gnum; i++){std::string name = "thread-" + std::to_string(i + 1);threads.emplace_back(name, Print);sleep(1);}// 統一啟動for (auto &thread : threads){thread.Start();}sleep(10);// 統一結束for (auto &thread : threads){thread.Stop();}// 等待線程等待for (auto &thread : threads){thread.Join();}return 0;}
運行結果: 線程統一啟動,10s后,線程集體結束,再集體被Joined
線程互斥
線程能夠看到的資源--共享資源
往往我們需要對這個共享資源進行保護
進程線程間的互斥相關背景概念
臨界資源:多線程執行流共享的資源就叫做臨界資源
臨界區:每個線程內部,訪問臨界資源的代碼,就叫做臨界區互斥:任何時刻,互斥保證有且只有一個執行流進入臨界區,訪問臨界資源,通常對臨界資源起保護作用
原子性:不會被任何調度機制打斷的操作,該操作只有兩態,要么完成,要么未完成
互斥量mutex
大部分情況,線程使用的數據都是局部變量,變量的地址空間在線程棧空間內,這種情況,變量歸屬單個線程,其他線程無法獲得這種變量。
但有時候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過數據的共享,完成線程之間的交互。多個線程并發的操作共享變量,會帶來一些問題。
搶票程序:
可重入VS線程安全
概念
線程安全:多個線程并發同一段代碼時,不會出現不同的結果。常見對全局變量或者靜態變量進行操作,并且沒有鎖保護的情況下,會出現該問題。
重入:同一個函數被不同的執行流調用,當前一個流程還沒有執行完,就有其他的執行流再次進入,我們稱之為重入。一個函數在重入的情況下,運行結果不會出現任何不同或者任何問題,則該函數被稱為可重入函數,否則,是不可重入函數。
常見的線程不安全的情況
- 不保護共享變量的函數
- 函數狀態隨著被調用,狀態發生變化的函數
- 返回指向靜態變量指針的函數
- 調用線程不安全函數的函數
常見鎖概念
死鎖
死鎖是指在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其他進程所站用不會釋放的資源而處于的一種永久等待狀態。
死鎖四個必要條件
互斥條件:一個資源每次只能被一個執行流使用
請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不放不剝奪條件:一個執行流已獲得的資源,在末使用完之前,不能強行剝奪循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源的關系
避免死鎖
- 破壞死鎖的四個必要條件加鎖順序一致
- 避免鎖未釋放的場景資源一次性分配
避免死鎖算法:死鎖檢測算法(了解)銀行家算法(了解)
以下所寫的搶票系統,哪個線程先搶,哪個線程后搶,是不確定的,整個線程的調度以及運行過程,完全是由調度器決定的。
#include<iostream>
#include"Thread.hpp"
#include<vector>
#include<unistd.h>
using namespace ThreadModle; int tickets = 10000;void route(const std::string &name)
{while(true){if(tickets > 0) //只有票數大于0的時候才需要搶票{// 搶票過程usleep(1000); // 1ms -> 搶票花費的時間printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);tickets--;}else{break;}}
}int main()
{Thread t1("thread-1", route);Thread t2("thread-2", route);Thread t3("thread-3", route);Thread t4("thread-4", route);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();
}
票數是10000張,因此不能讓票數減到負數,多線程在并發訪問共享資源時,錯誤或異常的問題:不僅搶到的負票,還搶到了同一張負票。
1.為什么會搶到負數
判斷的過程是否是計算?是 ---> 計算的結果是真假,是一種邏輯運算(計算機的運算類型還有算數運算)計算由CPU(應用程序被CPU調度)來做,tickets有空間和內容,剛開始存放在內存當中的一個變量。當需要做這個邏輯運算的時候,第一步,把數據(tickets的)從內存移動到CPU中的寄存器(eax),還需要有一個寄存器將符號的另一端的值(0,立即數)放進另一個寄存器,CPU對兩個寄存器中的值做邏輯判斷,是真為1,是假為0。得到結果后,CPU再控制執行流是執行if還是else。
在執行判斷的時候,會有我們設定的多個線程進入函數里進行搶票,每一個線程都要執行對應的判斷邏輯。CPU一般一直在執行某個線程代碼。CPU中的寄存器只有一套,而寄存器中的數據可有多套,每套數據屬于線程私有,當線程備切換的時候,線程會帶走自己的數據,線程回來的時候,會恢復寄存器中自己的一套數值。
假如現在是四個線程,并且只剩一張票了,線程a現在將tickets放進一個寄存器里,0也放進另一個寄存器,線程a正在被調度且已經判斷tickets = 1 > 0得到值是1,正準備執行搶票(printf)的時候(還未對tickets進行 --),被切換了,此時線程a會保存自己的上下文數據,以及判斷的結果res? = 1,線程b被叫醒進來搶票,此時的tickets仍然是1,邏輯判斷后,res = 1,此時(還未搶票和--tickets)有可能線程b也被切換。線程c、d來到,他們都執行如上操作,票數只有1張,但是卻進來了四個線程,線程a此時被喚醒,搶票后票數--,b進來,再--,tickets早已被-為負數。
tickets:1.重讀數據,2.--數據,3.寫回數據
總結:
- if 語句判斷條件為真以后,代碼可以并發的切換到其他線程
- usleep 這個模擬漫長業務的過程,在這個漫長的業務過程中,可能有很多個線程會進入該代碼段
- --ticket 操作本身就不是一個原子操作
要做到這三點,本質上就是需要一把鎖。Linux上提供的這把鎖叫互斥量。
1.?什么是互斥鎖?
定義:互斥鎖(Mutual Exclusion Lock)是一種同步機制,用于在多線程編程中保護共享資源,確保同一時間只有一個線程可以訪問臨界區(Critical Section)。
核心作用:防止多個線程同時修改共享數據,避免數據競爭(Race Condition)導致的不一致性。
2. 互斥鎖的核心接口(C語言,pthread庫)
在 Linux 中,互斥鎖通過?pthread
?庫實現。以下是主要接口函數:
1.?初始化互斥鎖
(1)?靜態初始化
適用于全局或靜態互斥鎖。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
特性:快速初始化,無需手動銷毀
pthread_mutex_destroy
。(2)?動態初始化
可自定義互斥鎖屬性(如設置遞歸鎖)。
pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); // 第二個參數為屬性,NULL 表示默認
必須銷毀:使用后需調用?
pthread_mutex_destroy
。操作成功都是返回1,操作失敗,返回-1
2.?加鎖與解鎖
(1)?阻塞加鎖
若鎖已被占用,當前線程會阻塞,直到鎖被釋放。
int pthread_mutex_lock(pthread_mutex_t *mutex);
返回值:成功返回?
0
,失敗返回錯誤碼(如未初始化的鎖返回?EINVAL
)。(2)?非阻塞加鎖
嘗試加鎖,若鎖被占用,立即返回錯誤碼?
EBUSY
。int pthread_mutex_trylock(pthread_mutex_t *mutex);
(3)?解鎖
釋放鎖,允許其他線程獲取。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
3.?銷毀互斥鎖
釋放動態初始化的鎖資源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
在最前面的搶票系統:
3.所謂的對臨界資源進行保護,本質是對臨界區代碼進行保護
全局的 tickets叫做共享資源,多線程未來都會執行同route,在線程執行的代碼之中,tickets臨界資源會被我們加以保護,這種被保護的全局資源就叫做臨界資源,在多線程所執行的代碼中一定會存在訪問臨界資源的代碼,訪問臨界資源的代碼就叫做臨界區,其他代碼叫做非臨界區。
我們對所有資源進行訪問,本質都是通過代碼進行訪問,因此要保護資源本質就是想辦法把訪問資源的代碼進行保護。
在臨界區的代碼只能串行執行
現在我們在剛才的搶票系統中添加鎖(加鎖和解鎖):
注意:
1.加鎖的范圍和粒度(臨界區包含的代碼的長度)一定要盡量小。
串行周期如果長,會導致整個系統的效率降低。
因此不將鎖加在循環外面(這會導致一個線程將所有的票都搶完,下一個線程才能進來,這是不合理的,不符合我們期待的多線程并發需求),解鎖也不能只加在break之后,會導致其他線程不能進來。而一個線程搶完票之后應該立馬讓另外的線程進來搶票,因此tickets--之后也要解鎖:
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;void route(const std::string &name) {while(true){pthread_mutex_lock(&gmutex);//加鎖if(tickets > 0) //只有票數大于0的時候才需要搶票{// 搶票過程usleep(1000); // 1ms -> 搶票花費的時間printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);tickets--;pthread_mutex_unlock(&gmutex);}else{pthread_mutex_unlock(&gmutex);break;}} }
2.任何線程,要進行搶票,都得先申請鎖,原則上,不應該有例外
3.所有的線程申請鎖,前提是所有的線程都能看到這把鎖,所鎖本身也是共享資源,因此加鎖的過程,必須是原子的。
4.原子性,要么不做,要做就做完,沒有中間狀態
5.如果線程申請鎖失敗了,線程被阻塞
6.如果線程申請成功了,就會繼續向后運行
7.如果線程申請鎖成功了,就開始執行臨界區代碼了,在執行臨界區代碼的期間,可以被切換嗎,是可以被切換的(對于CPU來說加鎖解鎖也不過就是像運算一樣的代碼),但是其他線程無法進入。假如我現在線程1正在執行,被切換走了,其他2,3,4線程能進來嗎,不能進來,因為我雖然被切換了,但是我沒有釋放鎖。一個線程在持有鎖的狀態下,可以放心的執行完臨界區代碼,幾遍被切換,其他線程無法進來,在我回來時又繼續執行代碼。
結論:所以對于其他線程,要么我沒有申請鎖,要么我釋放了鎖,對其他線程才有意義!這就相當于,我訪問臨界區,對其他線程就是原子的(要么是我解鎖了,要么就是我沒解鎖 ,我在中間發生任何事都對他們無關)。
修改線程封裝:
封裝一個線程數據(包括線程的名字以及線程的鎖(鎖傳地址)),未來想要創建一個線程,一方面在創建線程的時候傳遞線程名,傳遞一個回調方法,再傳遞一個線程參數(線程數據也就是)。
class ThreadData{public://未來給線程傳遞的數據類型ThreadData(const std::string &name, pthread_mutex_t *lock):_name(name), _lock(lock){}private:std::string name;pthread_mutex_t *lock; };// 線程要執行的方法typedef void (*func_t)(ThreadData *td); // 函數指針類型
創建多線程:
// 創建threadnum個線程
static int threadnum = 4;
int main()
{//創建的是局部鎖pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);// 創建多線程std::vector<Thread> threads;for (int i = 0; i < threadnum; i++){std::string name = "thread-" + std::to_string(i + 1);ThreadData *td = new ThreadData(name, &mutex); //將線程名字與鎖地址傳遞給ThreadDatathreads.emplace_back(name, route, td);//創建線程,將線程名字要執行的回調函數,以及回調函數的參數(線程參數)}//統一啟動for (auto &thread : threads){thread.Start();}// 等待線程等待for (auto &thread : threads){thread.Join();}pthread_mutex_destroy(&mutex);
}
實際情況下最好寫上private然后寫Get函數:
route函數:
void route(ThreadData *td)
{//檢驗是否訪問到的是同一把鎖// std::cout << td->_name <<",mutex address: " << td->_lock << std::endl;// sleep(1);while (true){pthread_mutex_lock(td->_lock); // 加鎖if (tickets > 0) // 只有票數大于0的時候才需要搶票{// 搶票過程usleep(1000); // 1ms -> 搶票花費的時間printf("who: %s, get a ticket: %d\n", td->_name.c_str(), tickets);tickets--;pthread_mutex_unlock(td->_lock);}else{pthread_mutex_unlock(td->_lock);break;}}
}
對鎖進行保護:
新建一個LockGuard.hpp文件:
#pragma once#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t *_mutex;
};
route代碼:注意看其中的注釋
void route(ThreadData *td)
{while (true)//是一個代碼塊{ //LockGuard是一個類型,定義出來的;臨時對象,會調用他的構造函數,自動進行加鎖,//while循環結束或者break結束,該對象臨時變量被釋放,析構函數被調用,解鎖//RAII風格的鎖LockGuard lockguard(td->_lock);//定義一個臨時對象,對區域進行保護if (tickets > 0){// 搶票過程usleep(1000); // 1ms -> 搶票花費的時間printf("who: %s, get a ticket: %d\n", td->_name.c_str(), tickets);tickets--;}else{break;}}
}
從原理角度理解這個鎖:
pthread_mutex_lock(&mutex);
如何理解申請鎖成功,允許你進入臨界區
如何理解申請鎖失敗,不允許你進入臨界區
允許我進入臨界區的本質就是申請鎖成功,pthread_mutex_lock()函數會返回
申請鎖失敗(鎖沒有就緒),pthread_mutex_lock()函數不返回,線程就是阻塞了。
pthread_mutex_lock()屬于pthread庫,線程也屬于pthread庫,所以在這個函數實現的時候,就是在做一個判斷,申請鎖是否成功,再設置線程的狀態,然后把線程放在全局的隊列當中。
一旦申請成功的線程把鎖pthread_mutex_ulock(),對應的在隊列當中的線程就會被喚醒,在重新pthread_mutex_lock()內部被重新喚醒,重新申請鎖。
最典型的就是scanf(檢測鍵盤是否輸入數據,沒輸入數據就被阻塞)
從實現角度理解鎖:
- 經過上面的例子,大家已經意識到單純的 i++ 或者 ++i 都不是原子的,有可能會有數據一致性問題
- 為了實現互斥鎖操作,大多數體系結構都提供了swap或exchange指令,該指令的作用是把寄存器和內存單元的數據相交換,由于只有一條指令,保證了原子性,即使是多處理器平臺,訪問內存的總線周期也有先后,一個處理器上的交換指令執行時另一個處理器的交換指令只能等待總線周期。 現在我們把lock和unlock的偽代碼改一下
加鎖(lock)邏輯
初始化:
movb $0, %al
?將?al
?寄存器設為?0
,為后續操作做準備。原子交換:
xchgb %al, mutex
?通過原子操作交換?al
?寄存器和內存中?mutex
?的值。
若?
mutex
?原值為?1
(未鎖定),交換后?mutex
?變為?0
(鎖定),al
?變為?1
。若?
mutex
?原值為?0
(已鎖定),交換后?mutex
?仍為?0
,al
?變為?0
。條件判斷:
若?
al > 0
:表示成功獲取鎖(原?mutex
?為?1
),返回?0
(成功)。否則:鎖已被占用,線程掛起等待,之后通過?
goto lock
?重新嘗試獲取鎖。解鎖(unlock)邏輯
釋放鎖:
movb $1, mutex
?將?mutex
?設為?1
,表示釋放鎖。喚醒線程:
注釋提示“喚醒等待Mutex的線程”,表明釋放鎖時會通知其他等待線程繼續競爭鎖。返回成功:
返回?0
(操作成功)
1.CPU的寄存器只有一套,被所有的線程共享,但是寄存器里面的數據,屬于執行流的上下文,屬于執行流私有的數據
2.CPU在執行代碼的時候,一定要有對應的執行載體 線程&&進程
3.數據在內存中,被所有線程共享的。
結論:把數據從內存移動到寄存器,本質是把數據從共享,變成線程的私有
結語:
? ? ? ?隨著這篇關于題目解析的博客接近尾聲,我衷心希望我所分享的內容能為你帶來一些啟發和幫助。學習和理解的過程往往充滿挑戰,但正是這些挑戰讓我們不斷成長和進步。我在準備這篇文章時,也深刻體會到了學習與分享的樂趣。
? ? ? ? ?在此,我要特別感謝每一位閱讀到這里的你。是你的關注和支持,給予了我持續寫作和分享的動力。我深知,無論我在某個領域有多少見解,都離不開大家的鼓勵與指正。因此,如果你在閱讀過程中有任何疑問、建議或是發現了文章中的不足之處,都歡迎你慷慨賜教。
? ? ? ? 你的每一條反饋都是我前進路上的寶貴財富。同時,我也非常期待能夠得到你的點贊、收藏,關注這將是對我莫大的支持和鼓勵。當然,我更期待的是能夠持續為你帶來有價值的內容,讓我們在知識的道路上共同前行。