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

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

注:
- 加鎖、解鎖是比較耗費系統(tǒng)資源的,會在一定程序上降低程序的運行速度
- 加鎖后的代碼是串行化執(zhí)行的,勢必會影響多線程場景中的運行速度
- 所以為了盡可能的降低影響,加鎖粒度要盡可能的細
02. 多線程案例
2.1 為什么線程需要互斥?
當多個線程同時訪問共享資源時,可能導致競態(tài)條件,造成數(shù)據(jù)不一致或程序異常。但有時候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過數(shù)據(jù)的共享,完成線程之間的交互。而多個線程并發(fā)的操作共享變量,會帶來一些問題。線程互斥機制確保在任何時刻只有一個線程能訪問共享資源。
#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);
// 理論結果為:200000
printf("Final counter: %d\n", counter); // 實際輸出通常小于200000
return 0;
}

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

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

細節(jié): 互斥會給其他線程帶來影響
當某個線程持有[鎖資源】 時,對于其他線程的有意義的狀態(tài):在這兩種狀態(tài)的劃分下,確保了多線程并發(fā)訪問時的 原子性
- 鎖被我申請了(其他線程無法獲取)
- 鎖被我釋放了(其他線程可以獲取鎖)
3.3 互斥鎖原理
lock是原子的,其他線程無法進入。 為了實現(xiàn)互斥鎖操作,大多數(shù)體系結構都提供了swap或exchange指令,該指令的作用是把寄存器和內存單元的數(shù)據(jù)交換(私有和共享),由于只有一條指令,保證了原子性,即使是多處理器平臺,訪問內存的總線周期也有先后,一個處理器上的交換指令執(zhí)行時另一個處理器的交換指令只能等待總線周期。
3.4 多線程封裝
著手編寫一個小組件: Demo 版線程庫目標:對 原生線程庫 提供的接口進行封裝,進一步提高對線程相關接口的熟練程度既然是封裝,這里的類成員包括:
- 線程
ID - 線程名
name - 線程狀態(tài)
status - 線程回調函數(shù)
fun t - 傳遞給回調函數(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;
}
// 回調方法
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 是一個指向當前類類型的常量指針
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()
{ // 亦指在外調用的線程處理函數(shù),_args與是否返回值有關
return _func(_args);
}
private:
pthread_t _tid; // 線程 ID
std::string _name; // 線程名
func_t _func; // 線程回調函數(shù)
void *_args; // 傳遞給回調函數(shù)的參數(shù)
};
測試代碼:
#include "thread.hpp"
// 1:線程創(chuàng)建和運行
void *basic_task(void *arg){
int *val = static_cast<int *>(arg);
std::cout << "線程正在運行,初始值為: " << *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; // 應該輸出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; // 記得釋放內存
}} return 0;}
結果如下:

3.5 互斥鎖封裝
我們對鎖進行封裝,實現(xiàn)一個簡單易用的小組件。利用創(chuàng)建對象時調用構造函數(shù),對象生命周期結束時調用析構函數(shù)的特點,融入加鎖、解鎖等操作。更加方便
#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; // 在該類下面定義了一個Mutex類型的引用成員變量,_mutex為變量名
};
3.5.1 RAII風格
像這種獲取資源即初始化的風格稱為RAII風格,非常巧妙的運用了類和對象的特性,實現(xiàn)半自動化操作。
04. 線程同步
當一個線程互斥地訪問某個變量時,它可能發(fā)現(xiàn)在其它線程改變狀態(tài)之前,它什么也做不了。例如:一個線程訪問隊列時,發(fā)現(xiàn)隊列為空,它只能等待,只到其它線程將一個節(jié)點添加到隊列中。這種情況就需要用到條件變量。
同步概念與競態(tài)條件:
- 同步:在保證數(shù)據(jù)安全的前提下,讓線程能夠按照某種特定的順序訪問臨界資源,從而有效避免饑餓問題,叫做同步
- 競態(tài)條件:因為時序問題,而導致程序異常,我們稱之為競態(tài)條件。在線程場景下,這種問題也不難理解
4.1 死鎖
死鎖是指在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其他進程所站用不會釋放的資源而處于的一種永久等待狀態(tài)。
4.1.1 死鎖四個必要條件
- 互斥條件:一個資源每次只能被一個執(zhí)行流使用
- 請求與保持條件:一個執(zhí)行流因請求資源而阻塞時,對已獲得的資源保持不放
- 不剝奪條件:一個執(zhí)行流已獲得的資源,在末使用完之前,不能強行剝奪
- 循環(huán)等待條件:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關系
4.1.2 避免死鎖
- 破壞死鎖的四個必要條件
- 加鎖順序一致
- 避免鎖未釋放的場景
- 資源一次性分配
4.1.3 避免死鎖算法
- 死鎖檢測算法(了解)
- 銀行家算法(了解
4.2 條件變量
條件變量是線程同步的高級機制,用于解決"等待特定條件成立"的場景。它總是與互斥鎖配合使用,實現(xiàn)高效的線程等待-通知機制。有以下特點:
- 總是與互斥鎖配合使用
- 解決"等待-通知"問題
- 避免忙等待(busy-waiting)
操作代碼:
// 初始化 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 等待條件滿足(自動釋放關聯(lián)互斥鎖) int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); // 通知條件 int pthread_cond_signal(pthread_cond_t *cond); // 喚醒一個線程 int pthread_cond_broadcast(pthread_cond_t *cond); // 廣播。。喚醒所有線程
可以把條件變量看作一個結構體,其中包含一個隊列結構,用來存儲正在排隊等候的線程信息,當條件滿足時,就會取 隊頭 線程進行操作,操作完成后重新進入隊尾。后續(xù)基于此實現(xià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; // 共享條件
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ù)準備時間
pthread_mutex_lock(&mutex);
printf("Producer: Data ready.\n");
data_ready = 1;
pthread_cond_signal(&cond); // 喚醒消費者
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;
}
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Tomcat無法加載css和js等靜態(tài)資源文件的解決思路
Tomcat無法加載css和js等靜態(tài)資源文件的情況想必從事相關行業(yè)的工作人員都有遇到過吧,接下來為大家介紹下詳細的解決方法,感興趣的朋友可以參考下2013-10-10
Linux gnome桌面環(huán)境切換KDE_Plasma實現(xiàn)過程
這篇文章主要介紹了Linux gnome桌面環(huán)境切換KDE_Plasma實現(xiàn)過程,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2026-01-01
Apache服務器關閉TRACE Method請求方式的方法
這篇文章主要介紹了Apache服務器關閉TRACE Method請求方式的方法,因為支持該方式的服務器存在跨站腳本漏洞,需要的朋友可以參考下2014-06-06
Windows下PHP安裝路徑配置錯誤導致Apache無法啟動的解決方法
這篇文章主要介紹了Windows下PHP安裝路徑配置錯誤導致Apache無法啟動的解決方法,本文可以說是一個引號引發(fā)的悲劇,需要的朋友可以參考下2014-09-09

