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

要想對counter++做修改,在底層被編譯成三條機(jī)器指令:
- 從內(nèi)存加載counter的值到寄存器(LOAD)
- 寄存器中的值加1(ADD)
- 將寄存器的值寫回內(nèi)存(STORE)
假設(shè)counter初始值為0,在兩個(gè)線程同時(shí)執(zhí)行的時(shí)候,可能出現(xiàn)下面這種情況。以至于多線程場景中對全局變量并發(fā)訪問不是 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)部,訪問臨界資源的代碼,就叫做臨界區(qū),例如上文中的
counter++。 - 互斥:任何時(shí)刻,互斥保證有且只有一個(gè)執(zhí)行流進(jìn)入臨界區(qū),訪問臨界資源,通常對臨界資源起保護(hù)作用
- 原子性(后面討論如何實(shí)現(xiàn)):不會被任何調(diào)度機(jī)制打斷的操作,該操作只有兩態(tài),要么完成,要么未完成
1.3 鎖的引入
對于臨界資源訪問時(shí)的安全問題,也可以通過加鎖來保證,實(shí)現(xiàn)多線程間的互斥訪問,互斥鎖就是解決多線程并發(fā)訪問方法之一。
我們可以在線程1進(jìn)入臨界區(qū)之前加鎖,出臨界區(qū)之后解鎖, 這樣可以確保并發(fā)訪問臨界資源時(shí)的線性進(jìn)行,若線程1在對共享資源進(jìn)行操作時(shí)被切換成線程2,線程2也只能阻塞等待解鎖。

注:
- 加鎖、解鎖是比較耗費(fèi)系統(tǒng)資源的,會在一定程序上降低程序的運(yùn)行速度
- 加鎖后的代碼是串行化執(zhí)行的,勢必會影響多線程場景中的運(yùn)行速度
- 所以為了盡可能的降低影響,加鎖粒度要盡可能的細(xì)
02. 多線程案例
2.1 為什么線程需要互斥?
當(dāng)多個(gè)線程同時(shí)訪問共享資源時(shí),可能導(dǎo)致競態(tài)條件,造成數(shù)據(jù)不一致或程序異常。但有時(shí)候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過數(shù)據(jù)的共享,完成線程之間的交互。而多個(gè)線程并發(fā)的操作共享變量,會帶來一些問題。線程互斥機(jī)制確保在任何時(shí)刻只有一個(gè)線程能訪問共享資源。
#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ù)是訪問臨界資源的代碼,亦稱為臨界區(qū)。
理想狀態(tài)下是希望兩個(gè)線程分別對counter加100000次。但是由由于非原子操作和內(nèi)存可見性問題,當(dāng)兩個(gè)線程同時(shí)執(zhí)行這些指令,可能會出現(xiàn)指令交錯(cuò),導(dǎo)致最終結(jié)果通常會小于預(yù)期200000。
要解決以上問題,需要做到三點(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ìn)程要調(diào)度時(shí)
- 通過sleep,從內(nèi)核返回用戶時(shí),會進(jìn)行時(shí)間片是否到達(dá)的檢測,進(jìn)而導(dǎo)致切換

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

細(xì)節(jié): 互斥會給其他線程帶來影響
當(dāng)某個(gè)線程持有[鎖資源】 時(shí),對于其他線程的有意義的狀態(tài):在這兩種狀態(tài)的劃分下,確保了多線程并發(fā)訪問時(shí)的 原子性
- 鎖被我申請了(其他線程無法獲取)
- 鎖被我釋放了(其他線程可以獲取鎖)
3.3 互斥鎖原理
lock是原子的,其他線程無法進(jìn)入。 為了實(shí)現(xiàn)互斥鎖操作,大多數(shù)體系結(jié)構(gòu)都提供了swap或exchange指令,該指令的作用是把寄存器和內(nèi)存單元的數(shù)據(jù)交換(私有和共享),由于只有一條指令,保證了原子性,即使是多處理器平臺,訪問內(nèi)存的總線周期也有先后,一個(gè)處理器上的交換指令執(zhí)行時(shí)另一個(gè)處理器的交換指令只能等待總線周期。
3.4 多線程封裝
著手編寫一個(gè)小組件: Demo 版線程庫目標(biāo):對 原生線程庫 提供的接口進(jìn)行封裝,進(jìn)一步提高對線程相關(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ù)編號寫入名字
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;
}
// 啟動線程
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ù)
};
測試代碼:
#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 互斥鎖封裝
我們對鎖進(jìn)行封裝,實(shí)現(xiàn)一個(gè)簡單易用的小組件。利用創(chuàng)建對象時(shí)調(diào)用構(gòu)造函數(shù),對象生命周期結(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)用了類和對象的特性,實(shí)現(xiàn)半自動化操作。
04. 線程同步
當(dāng)一個(gè)線程互斥地訪問某個(gè)變量時(shí),它可能發(fā)現(xiàn)在其它線程改變狀態(tài)之前,它什么也做不了。例如:一個(gè)線程訪問隊(duì)列時(shí),發(fā)現(xiàn)隊(duì)列為空,它只能等待,只到其它線程將一個(gè)節(jié)點(diǎn)添加到隊(duì)列中。這種情況就需要用到條件變量。
同步概念與競態(tài)條件:
- 同步:在保證數(shù)據(jù)安全的前提下,讓線程能夠按照某種特定的順序訪問臨界資源,從而有效避免饑餓問題,叫做同步
- 競態(tài)條件:因?yàn)闀r(shí)序問題,而導(dǎo)致程序異常,我們稱之為競態(tài)條件。在線程場景下,這種問題也不難理解
4.1 死鎖
死鎖是指在一組進(jìn)程中的各個(gè)進(jìn)程均占有不會釋放的資源,但因互相申請被其他進(jìn)程所站用不會釋放的資源而處于的一種永久等待狀態(tài)。
4.1.1 死鎖四個(gè)必要條件
- 互斥條件:一個(gè)資源每次只能被一個(gè)執(zhí)行流使用
- 請求與保持條件:一個(gè)執(zhí)行流因請求資源而阻塞時(shí),對已獲得的資源保持不放
- 不剝奪條件:一個(gè)執(zhí)行流已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪
- 循環(huán)等待條件:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關(guān)系
4.1.2 避免死鎖
- 破壞死鎖的四個(gè)必要條件
- 加鎖順序一致
- 避免鎖未釋放的場景
- 資源一次性分配
4.1.3 避免死鎖算法
- 死鎖檢測算法(了解)
- 銀行家算法(了解
4.2 條件變量
條件變量是線程同步的高級機(jī)制,用于解決"等待特定條件成立"的場景。它總是與互斥鎖配合使用,實(shí)現(xiàn)高效的線程等待-通知機(jī)制。有以下特點(diǎn):
- 總是與互斥鎖配合使用
- 解決"等待-通知"問題
- 避免忙等待(busy-waiting)
操作代碼:
// 初始化 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 等待條件滿足(自動釋放關(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),用來存儲正在排隊(duì)等候的線程信息,當(dāng)條件滿足時(shí),就會取 隊(duì)頭 線程進(jìn)行操作,操作完成后重新進(jìn)入隊(duì)尾。后續(xù)基于此實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模型。

簡單使用示例:
#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)文章
Tomcat無法加載css和js等靜態(tài)資源文件的解決思路
Tomcat無法加載css和js等靜態(tài)資源文件的情況想必從事相關(guān)行業(yè)的工作人員都有遇到過吧,接下來為大家介紹下詳細(xì)的解決方法,感興趣的朋友可以參考下2013-10-10
Linux gnome桌面環(huán)境切換KDE_Plasma實(shí)現(xiàn)過程
這篇文章主要介紹了Linux gnome桌面環(huán)境切換KDE_Plasma實(shí)現(xiàn)過程,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2026-01-01
Apache服務(wù)器關(guān)閉TRACE Method請求方式的方法
這篇文章主要介紹了Apache服務(wù)器關(guān)閉TRACE Method請求方式的方法,因?yàn)橹С衷摲绞降姆?wù)器存在跨站腳本漏洞,需要的朋友可以參考下2014-06-06
Windows下PHP安裝路徑配置錯(cuò)誤導(dǎo)致Apache無法啟動的解決方法
這篇文章主要介紹了Windows下PHP安裝路徑配置錯(cuò)誤導(dǎo)致Apache無法啟動的解決方法,本文可以說是一個(gè)引號引發(fā)的悲劇,需要的朋友可以參考下2014-09-09
Linux服務(wù)器配置SSH服務(wù)登錄失敗的解決方案
文章介紹了配置Linux服務(wù)器SSH遠(yuǎn)程登錄失敗處理機(jī)制的步驟,包括備份原配置文件、安裝pam_tally2統(tǒng)計(jì)登錄失敗次數(shù)、使用Sed命令編輯配置、重啟使配置生效以及封禁登錄失敗五次的賬號2026-03-03

