一、線程互斥
1. 線程互斥的引出
互斥
指的是一種機(jī)制,用于確保在同一時刻只有一個進(jìn)程或線程能夠訪問共享資源或執(zhí)行臨界區(qū)代碼。 互斥的目的是 防止多個并發(fā)執(zhí)行的進(jìn)程或線程訪問共享資源時產(chǎn)生競爭條件,從而保證數(shù)據(jù)的一致性和正確性
,下面我們來使用多線程來模擬實現(xiàn)一個搶票的場景,看看所產(chǎn)生的現(xiàn)象。
#include <iostream>
#include <cstring>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
#include "lockGuard.hpp"
#include "Thread.hpp"
using namespace std;
int tickets = 1000; // 加鎖保證共享資源的安全性
void* threadRoutine(void* args)
{
string name = static_cast<const char*>(args);
while(true)
{
if(tickets > 0)
{
usleep(2000); // 模擬搶票花費的時間
cout << name << " get a ticket: " << tickets-- << endl;
}
else
{
break;
}
usleep(1000);
}
return nullptr;
}
int main()
{
// 創(chuàng)建四個線程
pthread_t tids[4];
int n = sizeof(tids) / sizeof(tids[0]);
for(int i = 0; i < n; i++)
{
char* data = new char[64];
snprintf(data, 64, "thread-%d", i + 1);
pthread_create(tids + i, nullptr, threadRoutine, data);
}
for(int i = 0; i < 4; i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
這里我們可以看到,當(dāng)全局變量tickets
被幾個執(zhí)行流共享時,最后變成了-1,這是因為如果我們?nèi)绻褂枚嗑€程對一個全局變量修改時,線程之間會相互影響,導(dǎo)致線程安全問題。
下面我們來看一下當(dāng)多個線程對共享變量進(jìn)行修改時,為什么會發(fā)生上述的線程安全問題?
假設(shè)有一個全局變量 g_val
=100被兩個線程,線程A
和 線程B
共享,在多線程環(huán)境下分別對同一個全局變量g_val進(jìn)行操作。
當(dāng)對變量進(jìn)行操作時會分為三個步驟:
- CPU把內(nèi)存中的數(shù)據(jù)讀到寄存器里
- 在寄存器中對數(shù)據(jù)進(jìn)行計算
- 將修改后的數(shù)據(jù)從寄存器里寫回內(nèi)存
下面我們來看一下線程A和線程B對全局變量進(jìn)行操作時的過程:
-
線程A執(zhí)行g(shù)_val- -操作
當(dāng)線程A執(zhí)行完第二步時,正準(zhǔn)備執(zhí)行第三步時,時間片到了,線程A需要將自己的上下文和數(shù)據(jù)帶走。
此時的線程A認(rèn)為自己已經(jīng)將數(shù)據(jù)修改99了,當(dāng)下一次執(zhí)行時繼續(xù)執(zhí)行步驟三。 -
線程B在while中執(zhí)行g(shù)_val- -操作
線程B通過while循環(huán)了90次將g_val修改成了10,此時時間片到了。因此線程B也將自己的上下文保存了起來。
- 繼續(xù)執(zhí)行線程A
由于上次執(zhí)行線程A時第3步?jīng)]有執(zhí)行,所以線程A繼續(xù)執(zhí)行第3步。但是內(nèi)存中的g_val為上次線程B修改后的值10,所以線程A又將內(nèi)存中的值改成了99。
因此,一切的原因都是修改全局變量時線程調(diào)度切換、并發(fā)訪問進(jìn)而導(dǎo)致了數(shù)據(jù)不一致;想要解決這個問題,我們就需要進(jìn)行加鎖
保護(hù)。
2. 互斥量
要解決以上問題,需要做到三點:
- 代碼必須要有
互斥行為
:當(dāng)代碼進(jìn)入臨界區(qū)執(zhí)行時,不允許其他線程進(jìn)入該臨界區(qū)。- 如果多個線程同時要求執(zhí)行臨界區(qū)的代碼,并且臨界區(qū)沒有線程在執(zhí)行,那么只能允許一個線程進(jìn)入該臨界區(qū)。
- 如果線程不在臨界區(qū)中執(zhí)行,那么該線程不能阻止其他線程進(jìn)入臨界區(qū)。
要做到這三點,本質(zhì)上就是需要一把鎖。Linux上提供的這把鎖叫 互斥量
?? 初始化互斥量
- 靜態(tài)分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
- 動態(tài)分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 參數(shù):
mutex
:要初始化的互斥量attr
:NULL
?? 互斥量加鎖和解鎖
// 加鎖
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 解鎖
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 返回值:成功返回0,失敗返回錯誤號
?? 銷毀互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:
- 使用
PTHREAD_ MUTEX_ INITIALIZER
初始化的互斥量不需要銷毀- 不要銷毀一個已經(jīng)加鎖的互斥量
- 已經(jīng)銷毀的互斥量,要確保后面不會有線程再嘗試加鎖
調(diào)用 pthread_ lock
時,可能會遇到以下情況:
- 互斥量處于未鎖狀態(tài),該函數(shù)會將互斥量鎖定,同時返回成功
- 發(fā)起函數(shù)調(diào)用時,其他線程已經(jīng)鎖定互斥量,或者存在其他線程同時申請互斥量,但沒有競爭到互斥量,那么
pthread_ lock調(diào)用會陷入阻塞
(執(zhí)行流被掛起),等待互斥量解鎖
?? 下面我們來使用互斥鎖
來改進(jìn)一下改進(jìn)上面的售票系統(tǒng):
int tickets = 1000; // 加鎖保證共享資源的安全性
pthread_mutex_t mutex; // 定義一把鎖
void* threadRoutine(void* args)
{
string name = static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&mutex);
if(tickets > 0)
{
usleep(2000); // 模擬搶票花費的時間
cout << name << " get a ticket: " << tickets-- << endl;
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
usleep(1000);
}
return nullptr;
}
int main()
{
pthread_mutex_init(&mutex, nullptr); // 初始化鎖
// 創(chuàng)建四個線程
pthread_t tids[4];
int n = sizeof(tids) / sizeof(tids[0]);
for(int i = 0; i < n; i++)
{
char* data = new char[64];
snprintf(data, 64, "thread-%d", i + 1);
pthread_create(tids + i, nullptr, threadRoutine, data);
}
for(int i = 0; i < 4; i++)
{
pthread_join(tids[i], nullptr);
}
pthread_mutex_destroy(&mutex);
return 0;
}
因為加鎖會導(dǎo)致臨界區(qū)代碼串行訪問(互斥),從而導(dǎo)致代碼的執(zhí)行效率減低,因此我們在加鎖之后會發(fā)現(xiàn)代碼的運行速度比不加鎖之前慢了許多。因此,進(jìn)行加鎖訪問時,保證加鎖的粒度越小越好,不要將不訪問臨界區(qū)資源的代碼加鎖。
3. 互斥鎖的實現(xiàn)原理
互斥鎖的進(jìn)一步認(rèn)識:
- 加了鎖之后,線程在臨界區(qū)中也會被切換,但這樣也不會有問題。因為線程是帶著鎖進(jìn)行線程切換的,其余線程是無法申請到鎖的,無法進(jìn)入臨界區(qū)訪問臨界資源。
- 錯誤的編碼方式:線程不申請鎖直接訪問臨界區(qū)資源,這樣的話,就算別的線程持有鎖,該線程也可以進(jìn)入到臨界區(qū)。
- 在沒有持有鎖的線程看來,對該線程最有意義的情況只用兩種:
- 線程 1 沒有持有鎖(什么都沒做)
- 線程 1 釋放鎖(做完),此時我可以申請鎖。那么在線程 1 持有鎖的期間,所做的所有操作在其他線程看來都是原子的!
- 加鎖后,執(zhí)行臨界區(qū)的代碼一定是串行執(zhí)行的!
- 要訪問臨界資源,每一個線程都必須先申請鎖,那么每一個線程都必須先看到同一把鎖并訪問它,所以鎖本身也是一種共享資源。那么鎖肯定也要保護(hù)起來,為了保護(hù)鎖的安全,申請和釋放鎖的操作都必須是原子的!
互斥鎖的細(xì)節(jié):
- 凡是訪問同一個臨界資源的線程,都要進(jìn)行加鎖保護(hù),而且必須加同一把鎖,這個是一個游戲規(guī)則,不能有例外。
- 每一個線程訪問臨界區(qū)之前,得加鎖,加鎖本質(zhì)是給 臨界區(qū) 加鎖,加鎖的粒度盡量要細(xì)一些
- 線程訪問臨界區(qū)的時候,需要先加鎖->所有線程都必須要先看到同一把鎖->鎖本身就是公共資源->鎖如何保證自己的安全?-> 加鎖和解鎖本身就是原子的!
- 臨界區(qū)可以是一行代碼,可以是一批代碼,
a. 線程可能被切換嗎?當(dāng)然可能, 不要特殊化加鎖和解鎖,還有臨界區(qū)代碼。
b. 此時線程進(jìn)行切換會有影響嗎?不會,因為在我不在期間,任何人都沒有辦法進(jìn)入臨界區(qū),因為他無法成功的申請到鎖!因為鎖被我拿走了! - 這也正是體現(xiàn)互斥帶來的串行化的表現(xiàn),站在其他線程的角度,對其他線程有意義的狀態(tài)就是:鎖被我申請(持有鎖),鎖被我釋放了(不持有鎖), 原子性就體現(xiàn)在這里
- 解鎖的過程也被設(shè)計成為原子的!
互斥鎖的原理:
為了實現(xiàn)互斥鎖操作,大多數(shù)體系結(jié)構(gòu)都提供了 swap
或 exchange
指令,該指令的作用是把寄存器和內(nèi)存單元的數(shù)據(jù)相交換,由于只有一條指令,保證了原子性,即使是多處理器平臺,訪問內(nèi)存的 總線周期也有先后,一個處理器上的交換指令執(zhí)行時另一個處理器的交換指令只能等待總線周期。
下面我們來根據(jù)lock
和unlock
的偽代碼來分析一下加鎖和解鎖的過程:
線程A:
-
movb $0,al
調(diào)用線程,向自己的上下文寫入0 -
xchgb %al,mutex
將cpu的寄存器中的%al
與 內(nèi)存中的mutex
進(jìn)行交換,本質(zhì)是將共享數(shù)據(jù)交換到 自己的私有的上下文中。交換只有 一條匯編指令 ,要么沒交換,要不就交換完了,即加鎖的原子性 -
判斷al寄存器中的內(nèi)容是否大于0,如果大于0,證明加鎖成功。
線程B:
- 切換成線程B,繼續(xù)執(zhí)行前兩條指令,先將 al寄存器數(shù)據(jù)置為0,再將寄存器中的數(shù)據(jù) 與 內(nèi)存中的數(shù)據(jù)進(jìn)行交換。
-
接著判斷al寄存器中的內(nèi)容是否大于0,發(fā)現(xiàn)并不大于0,說明b申請鎖失敗,緊接著b線程被掛起等待,同時b的上下文隨著b的掛起被帶走。
-
當(dāng)A線程再次被切換回來時,繼續(xù)執(zhí)行上次還未執(zhí)行的判斷,發(fā)現(xiàn)al中的數(shù)據(jù)大于0,加鎖成功
-
線程A釋放鎖,
movb $1,mutex
將內(nèi)存中mutex的數(shù)據(jù)置為1,喚醒等待Mutex的線程,此時切換成線程B -
線程B執(zhí)行
lock
的前兩條指令,此時就可以加鎖成功了。
二、可重入和線程安全
線程安全
:多個線程并發(fā)同一段代碼時,不會出現(xiàn)不同的結(jié)果。常見對全局變量或者靜態(tài)變量進(jìn)行操作,并且沒有鎖保護(hù)的情況下,會出現(xiàn)該問題。重入
:同一個函數(shù)被不同的執(zhí)行流調(diào)用,當(dāng)前一個流程還沒有執(zhí)行完,就有其他的執(zhí)行流再次進(jìn)入,我們稱之為重入。一個函數(shù)在重入的情況下,運行結(jié)果不會出現(xiàn)任何不同或者任何問題,則該函數(shù)被稱為可重入函數(shù);否則,是不可重入函數(shù)。
?? 常見的線程不安全的情況:
- 不保護(hù)共享變量的函數(shù)
- 函數(shù)狀態(tài)隨著被調(diào)用,狀態(tài)發(fā)生變化的函數(shù)
- 返回指向靜態(tài)變量指針的函數(shù)
- 調(diào)用線程不安全函數(shù)的函數(shù)
?? 常見的線程安全的情況:
- 每個線程對全局變量或者靜態(tài)變量只有讀取的權(quán)限,而沒有寫入的權(quán)限,一般來說這些線程是安全的
- 類或者接口對于線程來說都是原子操作
- 多個線程之間的切換不會導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性
?? 常見的可重入的情況:
- 不使用全局變量或靜態(tài)變量
- 不使用用malloc或者new開辟出的空間
- 不調(diào)用不可重入函數(shù)
- 不返回靜態(tài)或全局?jǐn)?shù)據(jù),所有數(shù)據(jù)都有函數(shù)的調(diào)用者提供
- 使用本地數(shù)據(jù),或者通過制作全局?jǐn)?shù)據(jù)的本地拷貝來保護(hù)全局?jǐn)?shù)據(jù)
?? 常見的不可重入的情況:
- 調(diào)用了malloc/free函數(shù),因為malloc函數(shù)是用全局鏈表來管理堆的
- 調(diào)用了標(biāo)準(zhǔn)I/O庫函數(shù),標(biāo)準(zhǔn)I/O庫的很多實現(xiàn)都以不可重入的方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu)
- 可重入函數(shù)體內(nèi)使用了靜態(tài)的數(shù)據(jù)結(jié)構(gòu)
?? 可重入與線程安全的聯(lián)系:
- 函數(shù)是可重入的,那就是線程安全的。線程安全的函數(shù),不一定是可重入函數(shù)
- 函數(shù)是不可重入的,那就不能由多個線程使用,有可能引發(fā)線程安全問題(如:printf 函數(shù)是不可重入的,多線程向顯示器上打印數(shù)據(jù)時,數(shù)據(jù)可能會黏在一起)
- 如果一個函數(shù)中有全局變量,那么這個函數(shù)既不是線程安全也不是可重入的
三、線程和互斥鎖的封裝
1. 線程封裝
?? Threa.hpp
#pragma once
#include <iostream>
#include <cstdlib>
#include <string>
#include <pthread.h>
using namespace std;
class Thread
{
public:
typedef enum{
NEW = 0,
RUNNING,
EXITED
} ThreadStatus;
typedef void (*func_t)(void*);
public:
Thread(int num, func_t func, void* args) :_tid(0), _status(NEW),_func(func),_args(args)
{
char name[128];
snprintf(name, 128, "thread-%d", num);
_name = name;
}
int status(){ return _status; }
string threadname(){ return _name; }
pthread_t get_id()
{
if(_status == RUNNING)
return _tid;
else
return 0;
}
static void* thread_run(void* args)
{
Thread* ti = static_cast<Thread*>(args);
(*ti)();
return nullptr;
}
void operator()()
{
if(_func != nullptr)
_func(_args);
}
void run() // 封裝線程運行
{
int n = pthread_create(&_tid, nullptr, thread_run, this);
if(n != 0)
exit(-1);
_status = RUNNING; // 線程狀態(tài)變?yōu)檫\行
}
void join() // 瘋轉(zhuǎn)線程等待
{
int n = pthread_join(_tid, nullptr);
if(n != 0)
{
cout << "main thread join thread: " << _name << "error" << endl;
return;
}
_status = EXITED;
}
~Thread(){}
private:
pthread_t _tid;
string _name;
func_t _func; // 線程未來要執(zhí)行的回調(diào)
void* _args;
ThreadStatus _status;
};
1. 互斥鎖封裝
?? lockGuard.hpp
class Mutex // 自己不維護(hù)鎖,有外部傳入
{
public:
Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
{}
void lock()
{
pthread_mutex_lock(_pmutex);
}
void unlock()
{
pthread_mutex_unlock(_pmutex);
}
~Mutex()
{}
private:
pthread_mutex_t *_pmutex;
};
class LockGuard // 自己不維護(hù)鎖,有外部傳入
{
public:
LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
private:
Mutex _mutex;
};
四、死鎖
1. 死鎖的概念
死鎖
是指在一組進(jìn)程中的各個進(jìn)程均占有不會釋放的資源,但因互相申請被其他進(jìn)程所站用不會釋放的資源而處于的一種永久等待狀態(tài)。
下面我們通過一個小故事來讓大家理解一下死鎖:
有兩個小朋友張三和李四,共同去了一家商店,想要購買一塊1塊錢的棒棒糖,但是他們兩個各自都只有五毛錢。因此張三想要李四手里的五毛錢去買棒棒糖讓自己吃,但這時候李四就不樂意了,他也想想要張三手里的五毛錢去買棒棒糖讓自己吃。因此兩個人陷入了僵局,因此買棒棒糖吃這件事情就一直無法推進(jìn)下去。
- 兩個小朋友可以看作是兩個線程,兩個不同的小朋友可以看作兩把不同的鎖
- 棒棒糖是臨界資源,老板就是操作系統(tǒng)
- 想要訪問臨界資源,必須同時擁有兩把鎖
在操作系統(tǒng)中我們可以通過兩個線程的案例來理解死鎖:
雖然一般來說產(chǎn)生死鎖是因為兩把及兩把以上的鎖導(dǎo)致的,但是一把鎖也有可能會產(chǎn)生死鎖。
2. 死鎖的四個必要條件
互斥條件
:一個資源每次只能被一個執(zhí)行流使用請求與保持條件
:一個執(zhí)行流因請求資源而阻塞時,對已獲得的資源保持不放不剝奪條件
:一個執(zhí)行流已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪循環(huán)等待條件
:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關(guān)系
3. 避免死鎖
不加鎖
主動釋放鎖
(假設(shè)要有兩把鎖才能獲取臨界資源,本身有一把鎖,在多次申請另一把鎖時申請不到,就把自身的鎖釋放掉)按照順序申請鎖
(假設(shè)有線程A和B,線程A申請鎖時,必須保持先A再B,線程B申請鎖時,也必須保持先A再B
當(dāng)線程A申請到A鎖時,線程B也申請到A,就不會出現(xiàn)互相申請的情況了)控制線程統(tǒng)一釋放鎖
(將所有線程 申請的鎖 使用一個線程 全部釋放掉,就不會出現(xiàn)死鎖了)
證明
:一個線程申請的鎖,可以由另一個線程來釋放
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//一個線程加鎖, 另一個線程釋放鎖
void* threadRoutine(void* args)
{
cout << "I am a new thread" << endl;
pthread_mutex_lock(&mutex);
cout << "I get a mutex!" << endl;
pthread_mutex_lock(&mutex);
cout << "I alive again" << endl;
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);
sleep(3);
cout << "main thread run begin" << endl;
pthread_mutex_unlock(&mutex);
cout << "main thread unlock..." << endl;
sleep(3);
return 0;
}
由運行結(jié)果我們就可以看出,說明一個線程申請一把鎖,可以由另一個線程釋放。
五、線程同步
1. 線程同步的理解
互斥鎖
存在的兩種不合理的情況:
- 一個線程頻繁的申請到鎖,別人無法申請到鎖,導(dǎo)致別人饑餓的問題
- 上述的搶票系統(tǒng),修改一下,當(dāng)票數(shù)為0時,并不會立即退出。而是等待票數(shù)的增加,在等待票數(shù)增加的過程中,線程會頻繁的申請鎖和釋放鎖。這樣的情況會導(dǎo)致資源的浪費。
線程同步:
在保證數(shù)據(jù)安全的前提下,讓線程能夠按照某種特定的順序訪問臨界資源,從而有效避免饑餓問題,叫做線程同步。
當(dāng)我們訪問臨界資源前,需要先做臨界資源是否存在的檢測,檢測的本質(zhì)也是訪問臨界資源。那么對臨界資源的檢測也一定要在加鎖和解鎖之間。常規(guī)的方法檢測臨界資源是否就緒,就注定了我們必須頻繁地申請鎖和釋放鎖。
2. 條件變量
想要解決線程頻繁申請和釋放鎖的問題,需要做到以下兩點:
- 不要讓線程在頻繁的檢測資源是否就緒,而是讓線程在資源未就緒時進(jìn)行等待。
- 當(dāng)資源就緒的時候,通知等待該資源的線程,讓這些線程來進(jìn)行資源的申請和訪問。
達(dá)到以上兩點要求就是條件變量,條件變量可以通過允許線程阻塞和等待另一個線程發(fā)送信號來彌補(bǔ)互斥鎖的不足,所以互斥鎖和條件變量通常是一起使用的。
條件變量是一種線程同步機(jī)制,用于在多線程環(huán)境下實現(xiàn)線程間的協(xié)調(diào)與通信。他在處理競態(tài)條件和線程間的互斥等問題上具有重要作用。
?? 條件變量初始化
// 初始化方式一:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
// 初始化方式二:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
參數(shù)
cond
:要初始化的條件變量attr
:NULL
?? 條件變量銷毀
int pthread_cond_destroy(pthread_cond_t *cond)
?? 等待條件滿足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
參數(shù):文章來源:http://www.zghlxwxcb.cn/news/detail-709040.html
- cond:要在這個條件變量上等待
- mutex:互斥量
?? 喚醒等待文章來源地址http://www.zghlxwxcb.cn/news/detail-709040.html
int pthread_cond_broadcast(pthread_cond_t *cond); // 喚醒全部的線程
int pthread_cond_signal(pthread_cond_t *cond); // 喚醒該條件變量下等待的線程
#include <iostream>
#include <cstdio>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
const int num = 5;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* active(void* args)
{
string name = static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);// pthread_cond_wait,調(diào)用的時候,會自動釋放鎖
cout << name << "活動" << endl;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t tids[num];
for(int i = 0; i < num; i++)
{
char* name = new char[32];
snprintf(name, 32, "pthread-%d", i + 1);
pthread_create(tids + i, nullptr, active, name);
}
sleep(3);
while(true)
{
cout << "main thread wakeup other thread..." << endl;
pthread_cond_broadcast(&cond);
sleep(1);
}
for(int i = 0; i < num; i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
到了這里,關(guān)于【Linux】多線程互斥與同步的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!