Linux線程同步/互斥過(guò)程詳解
01. 資源共享問(wèn)題
1.1 多線程并發(fā)訪問(wèn)
例: 初始狀態(tài):counter=0,線程 1 和 2 各自都執(zhí)行counter++操作

要想對(duì)counter++做修改,在底層被編譯成三條機(jī)器指令:
- 從內(nèi)存加載counter的值到寄存器(LOAD)
- 寄存器中的值加1(ADD)
- 將寄存器的值寫回內(nèi)存(STORE)
假設(shè)counter初始值為0,在兩個(gè)線程同時(shí)執(zhí)行的時(shí)候,可能出現(xiàn)下面這種情況。以至于多線程場(chǎng)景中對(duì)全局變量并發(fā)訪問(wèn)不是 100%可靠的。
線程 1 執(zhí)行:
- 從內(nèi)存讀取
counter=0到寄存器。 - 寄存器中
counter+1=1,未寫回內(nèi)存,就切換到另外一個(gè)線程。
線程 2 執(zhí)行:
- 從內(nèi)存讀取
counter=0(因線程 1 未更新內(nèi)存)。 - 寄存器中
counter+1=1,寫回內(nèi)存,此時(shí)counter=1。
線程 1 恢復(fù)執(zhí)行:
- 將寄存器中已計(jì)算的
1寫回內(nèi)存,覆蓋線程 2 的更新。
最終結(jié)果:counter=1(預(yù)期應(yīng)為 2)。
1.2 臨界區(qū)與臨界資源
- 臨界資源:多線程執(zhí)行流共享的資源就叫做臨界資源
- 臨界區(qū):每個(gè)線程內(nèi)部,訪問(wèn)臨界資源的代碼,就叫做臨界區(qū),例如上文中的
counter++。 - 互斥:任何時(shí)刻,互斥保證有且只有一個(gè)執(zhí)行流進(jìn)入臨界區(qū),訪問(wèn)臨界資源,通常對(duì)臨界資源起保護(hù)作用
- 原子性(后面討論如何實(shí)現(xiàn)):不會(huì)被任何調(diào)度機(jī)制打斷的操作,該操作只有兩態(tài),要么完成,要么未完成
1.3 鎖的引入
對(duì)于臨界資源訪問(wèn)時(shí)的安全問(wèn)題,也可以通過(guò)加鎖來(lái)保證,實(shí)現(xiàn)多線程間的互斥訪問(wèn),互斥鎖就是解決多線程并發(fā)訪問(wèn)方法之一。
我們可以在線程1進(jìn)入臨界區(qū)之前加鎖,出臨界區(qū)之后解鎖, 這樣可以確保并發(fā)訪問(wèn)臨界資源時(shí)的線性進(jìn)行,若線程1在對(duì)共享資源進(jìn)行操作時(shí)被切換成線程2,線程2也只能阻塞等待解鎖。

注:
- 加鎖、解鎖是比較耗費(fèi)系統(tǒng)資源的,會(huì)在一定程序上降低程序的運(yùn)行速度
- 加鎖后的代碼是串行化執(zhí)行的,勢(shì)必會(huì)影響多線程場(chǎng)景中的運(yùn)行速度
- 所以為了盡可能的降低影響,加鎖粒度要盡可能的細(xì)
02. 多線程案例
2.1 為什么線程需要互斥?
當(dāng)多個(gè)線程同時(shí)訪問(wèn)共享資源時(shí),可能導(dǎo)致競(jìng)態(tài)條件,造成數(shù)據(jù)不一致或程序異常。但有時(shí)候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過(guò)數(shù)據(jù)的共享,完成線程之間的交互。而多個(gè)線程并發(fā)的操作共享變量,會(huì)帶來(lái)一些問(wèn)題。線程互斥機(jī)制確保在任何時(shí)刻只有一個(gè)線程能訪問(wèn)共享資源。
#include <stdio.h>
#include <pthread.h>
int counter = 0;
void* increment(void* arg) {
for (int i = 0; i < 100000; i++) {
counter++; // 非原子操作}
return NULL;}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 理論結(jié)果為:200000
printf("Final counter: %d\n", counter); // 實(shí)際輸出通常小于200000
return 0;
}

在上面代碼里面,我們知道counter是臨界資源,而increment函數(shù)是訪問(wèn)臨界資源的代碼,亦稱為臨界區(qū)。
理想狀態(tài)下是希望兩個(gè)線程分別對(duì)counter加100000次。但是由由于非原子操作和內(nèi)存可見性問(wèn)題,當(dāng)兩個(gè)線程同時(shí)執(zhí)行這些指令,可能會(huì)出現(xiàn)指令交錯(cuò),導(dǎo)致最終結(jié)果通常會(huì)小于預(yù)期200000。
要解決以上問(wèn)題,需要做到三點(diǎn):
- 代碼必須要有互斥行為:當(dāng)代碼進(jìn)入臨界區(qū)執(zhí)行時(shí),不允許其他線程進(jìn)入該臨界區(qū)。
- 如果多個(gè)線程同時(shí)要求執(zhí)行臨界區(qū)的代碼,并且臨界區(qū)沒有線程在執(zhí)行,那么只能允許一個(gè)線程進(jìn)入該臨界區(qū)。
- 如果線程不在臨界區(qū)中執(zhí)行,那么該線程不能阻止其他線程進(jìn)入臨界區(qū)。
要做到這三點(diǎn),本質(zhì)上就是需要一把鎖。Linux上提供的這把鎖叫互斥量
2.2 線程或進(jìn)程切換時(shí)機(jī)?
- 時(shí)間片耗盡時(shí)
- 有更高優(yōu)先級(jí)的進(jìn)程要調(diào)度時(shí)
- 通過(guò)sleep,從內(nèi)核返回用戶時(shí),會(huì)進(jìn)行時(shí)間片是否到達(dá)的檢測(cè),進(jìn)而導(dǎo)致切換

如果鎖對(duì)象是全局的或靜態(tài)的,可以用宏:PTHREAD_MUTEX_INITIALIZER初始化,并且不用我們主動(dòng)destroy;如果鎖對(duì)象是局部的,需要用pthread_mutex_init初始化,用pthread_mutex_destroy釋放。
- 所有對(duì)資源的保護(hù),都是對(duì)臨界區(qū)代碼的訪問(wèn),因?yàn)橘Y源都是通過(guò)代碼訪問(wèn)的。
- 要保證加鎖的細(xì)粒度。
- 加鎖就是找到臨界區(qū),對(duì)臨界區(qū)進(jìn)行加鎖。
那么相應(yīng)的又有一些問(wèn)題:
- 鎖也是全局的共享資源,誰(shuí)保證鎖的安全?加鎖和解鎖被設(shè)計(jì)為原子的。
- 如果看待鎖?加鎖本質(zhì)就是對(duì)資源的預(yù)定工作,整體使用資源,所以加鎖前先要申請(qǐng)鎖。
- 如果申請(qǐng)鎖的時(shí)候,鎖已經(jīng)被別的線程拿走了怎么辦?其他線程阻塞等待。
- 線程在訪問(wèn)臨界區(qū)的時(shí)候,可不可以被切換?可以,我被切走,其他線程也不能進(jìn)來(lái),因?yàn)槲易叩臅r(shí)候是帶著鎖走的,保證了原子性。
03. 線程互斥
3.1 互斥鎖操作
有以下特點(diǎn):
- 最簡(jiǎn)單的同步原語(yǔ)
- 只有"鎖定"和"未鎖定"兩種狀態(tài)
- 同一時(shí)間只允許一個(gè)線程持有鎖
// 初始化(靜態(tài)) pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 初始化(動(dòng)態(tài)) int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); // 加鎖/解鎖 int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); // 銷毀 int pthread_mutex_destroy(pthread_mutex_t *mutex);
3.2代碼互斥問(wèn)題優(yōu)化
通過(guò)對(duì)上面代碼進(jìn)行改進(jìn),我們便可以得到正確的結(jié)果。

細(xì)節(jié): 互斥會(huì)給其他線程帶來(lái)影響
當(dāng)某個(gè)線程持有[鎖資源】 時(shí),對(duì)于其他線程的有意義的狀態(tài):在這兩種狀態(tài)的劃分下,確保了多線程并發(fā)訪問(wèn)時(shí)的 原子性
- 鎖被我申請(qǐng)了(其他線程無(wú)法獲取)
- 鎖被我釋放了(其他線程可以獲取鎖)
3.3 互斥鎖原理
lock是原子的,其他線程無(wú)法進(jìn)入。 為了實(shí)現(xiàn)互斥鎖操作,大多數(shù)體系結(jié)構(gòu)都提供了swap或exchange指令,該指令的作用是把寄存器和內(nèi)存單元的數(shù)據(jù)交換(私有和共享),由于只有一條指令,保證了原子性,即使是多處理器平臺(tái),訪問(wèn)內(nèi)存的總線周期也有先后,一個(gè)處理器上的交換指令執(zhí)行時(shí)另一個(gè)處理器的交換指令只能等待總線周期。
3.4 多線程封裝
著手編寫一個(gè)小組件: Demo 版線程庫(kù)目標(biāo):對(duì) 原生線程庫(kù) 提供的接口進(jìn)行封裝,進(jìn)一步提高對(duì)線程相關(guān)接口的熟練程度既然是封裝,這里的類成員包括:
- 線程
ID - 線程名
name - 線程狀態(tài)
status - 線程回調(diào)函數(shù)
fun t - 傳遞給回調(diào)函數(shù)的參數(shù)
args
3.4.1 thread.hpp編寫
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <cassert>
// 參數(shù)、返回值為 void 的函數(shù)類型
typedef void *(*func_t)(void *);
const int num = 1024;
class Thread
{
public:
Thread(func_t func, void *args = nullptr, int number = 0)
: _func(func), _args(args)
{
// 根據(jù)編號(hào)寫入名字
char buf[128];
snprintf(buf, sizeof buf, "thread-%d", num);
_name = buf;
int n = pthread_create(&_tid, nullptr, runHelper, this); // this->Thread*
assert(n == 0);
(void)n;
}
// 回調(diào)方法
static void *runHelper(void *args)
{
Thread *_this = static_cast<Thread *>(args);
return _this->callback();
}
// 獲取 ID
pthread_t getTID() const
{
return _tid;
}
// 獲取線程名
std::string getName() const
{
return _name;
}
// 啟動(dòng)線程
void run()
{
int ret = pthread_create(&_tid, nullptr, runHelper, this );//this 是一個(gè)指向當(dāng)前類類型的常量指針
if (ret != 0)
{
std::cerr << "create thread fail!" << std::endl;
exit(1); // 創(chuàng)建線程失敗,直接退出
}
}
// 線程等待
void join()
{
int ret = pthread_join(_tid, nullptr);
if (ret != 0)
{
std::cerr << "thread join fail!" << std::endl;
exit(1); // 等待失敗,直接退出
}
}
void *callback()
{ // 亦指在外調(diào)用的線程處理函數(shù),_args與是否返回值有關(guān)
return _func(_args);
}
private:
pthread_t _tid; // 線程 ID
std::string _name; // 線程名
func_t _func; // 線程回調(diào)函數(shù)
void *_args; // 傳遞給回調(diào)函數(shù)的參數(shù)
};
測(cè)試代碼:
#include "thread.hpp"
// 1:線程創(chuàng)建和運(yùn)行
void *basic_task(void *arg){
int *val = static_cast<int *>(arg);
std::cout << "線程正在運(yùn)行,初始值為: " << *val << std::endl;
*val *= 2; // 修改傳入的值
return nullptr;}
// 2:帶返回值
void *task_with_return(void *arg){
std::string *msg = new std::string("Hello!");
return msg;}
int main(){{
int value = 42;
Thread t1(basic_task, &value);
t1.join();
std::cout << "修改后旳值為: " << value << std::endl; // 應(yīng)該輸出84}
std::cout << "---------------: " << std::endl;{
Thread t2(task_with_return);
void *ret_val = nullptr;
pthread_join(t2.getTID(), &ret_val); // 直接使用pthread_join獲取返回值
if (ret_val){
std::string *msg = static_cast<std::string *>(ret_val);
std::cout << *msg << std::endl; // 輸出線程返回的消息
delete msg; // 記得釋放內(nèi)存
}} return 0;}
結(jié)果如下:

3.5 互斥鎖封裝
我們對(duì)鎖進(jìn)行封裝,實(shí)現(xiàn)一個(gè)簡(jiǎn)單易用的小組件。利用創(chuàng)建對(duì)象時(shí)調(diào)用構(gòu)造函數(shù),對(duì)象生命周期結(jié)束時(shí)調(diào)用析構(gòu)函數(shù)的特點(diǎn),融入加鎖、解鎖等操作。更加方便
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex(const Mutex &) = delete;
const Mutex &operator=(const Mutex &) = delete;
Mutex(){
int n = pthread_mutex_init(&_lock, nullptr);
}
void Lock(){
int n = pthread_mutex_lock(&_lock);
}
void Unlock(){
int n = pthread_mutex_unlock(&_lock);
}
pthread_mutex_t *LockPtr() { return &_lock; }
~Mutex(){
int n = pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard{
public:
LockGuard(Mutex &mutex)
: _mutex(mutex){
_mutex.Lock();
}
~LockGuard(){
_mutex.Unlock();
}
private:
Mutex &_mutex; // 在該類下面定義了一個(gè)Mutex類型的引用成員變量,_mutex為變量名
};
3.5.1 RAII風(fēng)格
像這種獲取資源即初始化的風(fēng)格稱為RAII風(fēng)格,非常巧妙的運(yùn)用了類和對(duì)象的特性,實(shí)現(xiàn)半自動(dòng)化操作。
04. 線程同步
當(dāng)一個(gè)線程互斥地訪問(wèn)某個(gè)變量時(shí),它可能發(fā)現(xiàn)在其它線程改變狀態(tài)之前,它什么也做不了。例如:一個(gè)線程訪問(wèn)隊(duì)列時(shí),發(fā)現(xiàn)隊(duì)列為空,它只能等待,只到其它線程將一個(gè)節(jié)點(diǎn)添加到隊(duì)列中。這種情況就需要用到條件變量。
同步概念與競(jìng)態(tài)條件:
- 同步:在保證數(shù)據(jù)安全的前提下,讓線程能夠按照某種特定的順序訪問(wèn)臨界資源,從而有效避免饑餓問(wèn)題,叫做同步
- 競(jìng)態(tài)條件:因?yàn)闀r(shí)序問(wèn)題,而導(dǎo)致程序異常,我們稱之為競(jìng)態(tài)條件。在線程場(chǎng)景下,這種問(wèn)題也不難理解
4.1 死鎖
死鎖是指在一組進(jìn)程中的各個(gè)進(jìn)程均占有不會(huì)釋放的資源,但因互相申請(qǐng)被其他進(jìn)程所站用不會(huì)釋放的資源而處于的一種永久等待狀態(tài)。
4.1.1 死鎖四個(gè)必要條件
- 互斥條件:一個(gè)資源每次只能被一個(gè)執(zhí)行流使用
- 請(qǐng)求與保持條件:一個(gè)執(zhí)行流因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放
- 不剝奪條件:一個(gè)執(zhí)行流已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪
- 循環(huán)等待條件:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關(guān)系
4.1.2 避免死鎖
- 破壞死鎖的四個(gè)必要條件
- 加鎖順序一致
- 避免鎖未釋放的場(chǎng)景
- 資源一次性分配
4.1.3 避免死鎖算法
- 死鎖檢測(cè)算法(了解)
- 銀行家算法(了解
4.2 條件變量
條件變量是線程同步的高級(jí)機(jī)制,用于解決"等待特定條件成立"的場(chǎng)景。它總是與互斥鎖配合使用,實(shí)現(xiàn)高效的線程等待-通知機(jī)制。有以下特點(diǎn):
- 總是與互斥鎖配合使用
- 解決"等待-通知"問(wèn)題
- 避免忙等待(busy-waiting)
操作代碼:
// 初始化 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 等待條件滿足(自動(dòng)釋放關(guān)聯(lián)互斥鎖) int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); // 通知條件 int pthread_cond_signal(pthread_cond_t *cond); // 喚醒一個(gè)線程 int pthread_cond_broadcast(pthread_cond_t *cond); // 廣播。。喚醒所有線程
可以把條件變量看作一個(gè)結(jié)構(gòu)體,其中包含一個(gè)隊(duì)列結(jié)構(gòu),用來(lái)存儲(chǔ)正在排隊(duì)等候的線程信息,當(dāng)條件滿足時(shí),就會(huì)取 隊(duì)頭 線程進(jìn)行操作,操作完成后重新進(jìn)入隊(duì)尾。后續(xù)基于此實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模型。

簡(jiǎn)單使用示例:
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//靜態(tài)初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int data_ready = 0; // 共享?xiàng)l件
void* consumer(void* arg) {
pthread_mutex_lock(&mutex);
while (data_ready == 0) {
printf("Consumer: Waiting...\n");
pthread_cond_wait(&cond, &mutex); // 阻塞并釋放鎖
}
printf("Consumer: Processing data.\n");
data_ready = 0;
pthread_mutex_unlock(&mutex);
return NULL;
}
void* producer(void* arg) {
sleep(1); // 模擬數(shù)據(jù)準(zhǔn)備時(shí)間
pthread_mutex_lock(&mutex);
printf("Producer: Data ready.\n");
data_ready = 1;
pthread_cond_signal(&cond); // 喚醒消費(fèi)者
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, consumer, NULL);
pthread_create(&tid2, NULL, producer, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
apache中使用mod_log_slow分析響應(yīng)慢的請(qǐng)求
這篇文章主要介紹了apache中使用mod_log_slow分析響應(yīng)慢的請(qǐng)求,使用mod_log_slow可以定位到響應(yīng)慢的PHP代碼位置,需要的朋友可以參考下2014-06-06
Ubuntu16.04.4LTS安裝mininet遇到的問(wèn)題及解決方案
今天小編就為大家分享一篇關(guān)于Ubuntu16.04.4LTS安裝mininet遇到的問(wèn)題及解決方案,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-10-10
linux后臺(tái)運(yùn)行的幾種方式(小結(jié))
這篇文章主要介紹了linux后臺(tái)運(yùn)行的幾種方式(小結(jié)),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
Linux下安裝Oracle(CentOS-Oracle 12c)的方法
這篇文章主要介紹了Linux下安裝Oracle(CentOS-Oracle 12c)的方法,本文實(shí)例講解,介紹的非常詳細(xì),具有參考借鑒價(jià)值,感興趣的朋友一起看看吧2016-11-11
linux中關(guān)于ftp查看不到文件列表的問(wèn)題詳解
下面小編就為大家?guī)?lái)一篇linux中關(guān)于ftp查看不到文件列表的問(wèn)題詳解。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-11-11

