📢博客主頁:https://blog.csdn.net/2301_779549673
📢歡迎點贊 👍 收藏 ?留言 📝 如有錯誤敬請指正!
📢本文由 JohnKi 原創,首發于 CSDN🙉
📢未來很長,值得我們全力奔赴更美好的生活?
文章目錄
- 📢前言
- 🏳??🌈一、從場景看互斥:為什么需要鎖?
- 🏳??🌈二、互斥鎖核心概念圖解
- 2.1 臨界區與非臨界區
- 2. 2 進程線程間的互斥相關背景概念
- 🏳??🌈三、Linux互斥鎖原理剖析
- 3.2 初始化互斥量
- 3.3 銷毀互斥量
- 3.3 pthread_mutex 底層實現
- 🏳??🌈四、C++ RAII封裝實戰
- 4.1 基礎互斥類(Mutex)
- 4.2 守衛鎖(LockGuard)
- 🏳??🌈五、完整代碼
- 5.1 Mutex.hpp
- 5.2 Mutex.cc
- 5.3 Makefile
- 👥總結
📢前言
緊接上回的線程C++封裝
,這回筆者著重介紹一下互斥的原理和其必要性,并手把手使用C++封裝一個RAII模型。
還有一點,筆者之后的封裝都會使用之前博客中封裝好的容器,需要的可以去倉庫或者前面的博客中自取。
RAII
的核心思想是將資源的獲取和初始化放在對象的構造函數中進行,而資源的釋放放在對象的析構函數中進行。當對象被創建時,其構造函數會自動執行,從而完成資源的獲取;當對象的生命周期結束時,其析構函數會被自動調用,從而完成資源的釋放。這樣,資源的生命周期就與對象的生命周期綁定在一起,利用 C++ 等語言的對象自動銷毀機制來確保資源的正確釋放。
🏳??🌈一、從場景看互斥:為什么需要鎖?
假設你的銀行賬戶余額是1000元,同時有兩個線程執行轉賬操作:
- ?線程A:存入200元 → balance += 200
- ?線程B:取出300元 → balance -= 300
無鎖情況下可能的執行順序:
線程A讀取balance(1000) → 線程B讀取balance(1000) →
線程A寫入1200 → 線程B寫入700
最終結果:700元(正確應為900元)
🏳??🌈二、互斥鎖核心概念圖解
2.1 臨界區與非臨界區
void* thread_func(void* arg) {// 非臨界區(可并發執行)prepare_data(); // 臨界區(需互斥訪問)pthread_mutex_lock(&mtx);update_shared_resource(); pthread_mutex_unlock(&mtx);// 非臨界區(可并發執行)post_process();
}
關鍵特征:
🔵 ?非臨界區:允許多線程并行(如圖中綠色區域)
🔴 ?臨界區:同一時刻僅一個線程執行(紅色區域)
2. 2 進程線程間的互斥相關背景概念
- 臨界資源:多線程執行流共享的資源就叫做臨界資源
- 臨界區:每個線程內部,訪問臨界資源的代碼,就叫做臨界區
- 互斥:任何時刻,互斥保證有且只有一個執行流進入臨界區,訪問臨界資源,通常對臨界資源起保護作用
- 原子性:(后面討論如何實現):不會被任何調度機制打斷的操作,該操作只有兩態,要么完成,要么未完成
🏳??🌈三、Linux互斥鎖原理剖析
核心操作流程:
sequenceDiagramparticipant 線程Aparticipant 互斥鎖participant 內核線程A->>互斥鎖: pthread_mutex_lock()alt 鎖空閑互斥鎖-->>線程A: 立即獲得鎖else 鎖被占線程A->>內核: 進入休眠隊列內核-->>線程A: 喚醒并獲取鎖end線程A->>臨界區: 執行操作線程A->>互斥鎖: pthread_mutex_unlock()
3.2 初始化互斥量
方法一:靜態分布
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法二:動態分布
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
mutex:要初始化的互斥量
attr:NULL
3.3 銷毀互斥量
銷毀互斥量需要注意:
- 使用 PTHREAD_MUTEXINITIALIZER 初始化的互斥量不需要銷毀
- 不要銷毀一個已經加鎖的互斥量
- 已經銷毀的互斥量,要確保后面不會有線程再嘗試加鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加鎖和解鎖
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失敗返回錯誤號
調? pthread_mutex_lock 時,可能會遇到以下情況:
- 互斥量處于未鎖狀態,該函數會將互斥量鎖定,同時返回成功
- 發起函數調用時,其他線程已經鎖定互斥量,或者存在其他線程同時申請互斥量,但沒有競爭到互斥量,那么pthread lock調用會陷入阻塞(執行流被掛起),等待互斥量解鎖。
3.3 pthread_mutex 底層實現
// 鎖結構(簡化為x86實現)
struct pthread_mutex {int __lock; // 鎖狀態標識int __count; // 遞歸鎖計數器int __owner; // 持有者線程IDint __kind; // 鎖類型標識// ... 其他字段
};
🏳??🌈四、C++ RAII封裝實戰
4.1 基礎互斥類(Mutex)
// 互斥鎖封裝類(不可拷貝構造/賦值)class Mutex{public:// 禁止拷貝(保護系統鎖資源)Mutex(const Mutex&) = delete;const Mutex& operator = (const Mutex&) = delete;// 構造函數:初始化POSIX互斥鎖Mutex(){// 初始化互斥鎖屬性為默認值int n = ::pthread_mutex_init(&_lock, nullptr);(void)n; // 實際開發建議處理錯誤碼}// 析構函數:銷毀鎖資源~Mutex(){// 確保鎖已處于未鎖定狀態int n = ::pthread_mutex_destroy(&_lock);(void)n; // 生產環境應檢查返回值}// 加鎖操作(阻塞直至獲取鎖)void Lock(){// 可能返回EDEADLK(死鎖檢測)等錯誤碼int n = ::pthread_mutex_lock(&_lock);(void)n; // 簡化處理,實際建議拋異常或記錄日志}// 解鎖操作(必須由鎖持有者調用)void Unlock(){// 未持有鎖時解鎖將返回EPERMint n = ::pthread_mutex_unlock(&_lock);(void)n; }private:pthread_mutex_t _lock; // 底層鎖對象};
4.2 守衛鎖(LockGuard)
守衛鎖不是新的鎖類型,而是對已有鎖的自動化生命周期管理工具。這種設計模式完美契合圖示中"Lock-unlock"邊界需要嚴格匹配的核心訴求
守衛鎖工作流程
sequenceDiagramparticipant 線程participant 守衛鎖participant 互斥鎖線程->>守衛鎖: 創建LockGuard對象守衛鎖->>互斥鎖: 調用Lock()互斥鎖-->>守衛鎖: 獲得鎖線程->>臨界區: 執行操作線程->>守衛鎖: 對象離開作用域守衛鎖->>互斥鎖: 調用Unlock()互斥鎖-->>其他線程: 釋放鎖資源
實現
// RAII鎖守衛(自動管理鎖生命周期)class LockGuard{public:// 構造時加鎖(必須傳入已初始化的Mutex引用)LockGuard(Mutex &mtx):_mtx(mtx){_mtx.Lock(); // 進入臨界區}// 析構時自動解鎖(異常安全保證)~LockGuard(){_mtx.Unlock(); // 離開作用域自動釋放}private:Mutex &_mtx; // 引用方式持有,避免拷貝導致未定義行為};
🏳??🌈五、完整代碼
5.1 Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h> // POSIX線程庫頭文件namespace LockModule
{// 互斥鎖封裝類(不可拷貝構造/賦值)class Mutex{public:// 禁止拷貝(保護系統鎖資源)Mutex(const Mutex&) = delete;const Mutex& operator = (const Mutex&) = delete;// 構造函數:初始化POSIX互斥鎖Mutex(){// 初始化互斥鎖屬性為默認值int n = ::pthread_mutex_init(&_lock, nullptr);(void)n; // 實際開發建議處理錯誤碼}// 析構函數:銷毀鎖資源~Mutex(){// 確保鎖已處于未鎖定狀態int n = ::pthread_mutex_destroy(&_lock);(void)n; // 生產環境應檢查返回值}// 加鎖操作(阻塞直至獲取鎖)void Lock(){// 可能返回EDEADLK(死鎖檢測)等錯誤碼int n = ::pthread_mutex_lock(&_lock);(void)n; // 簡化處理,實際建議拋異常或記錄日志}// 解鎖操作(必須由鎖持有者調用)void Unlock(){// 未持有鎖時解鎖將返回EPERMint n = ::pthread_mutex_unlock(&_lock);(void)n; }private:pthread_mutex_t _lock; // 底層鎖對象};// RAII鎖守衛(自動管理鎖生命周期)class LockGuard{public:// 構造時加鎖(必須傳入已初始化的Mutex引用)LockGuard(Mutex &mtx):_mtx(mtx){_mtx.Lock(); // 進入臨界區}// 析構時自動解鎖(異常安全保證)~LockGuard(){_mtx.Unlock(); // 離開作用域自動釋放}private:Mutex &_mtx; // 引用方式持有,避免拷貝導致未定義行為};
}
5.2 Mutex.cc
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>int ticket = 0;
pthread_mutex_t mutex;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void *route(void *arg)
{char *id = (char *)arg;while (1){pthread_mutex_lock(&mutex);if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;pthread_mutex_unlock(&mutex);}else{printf("%s wait on cond!\n", id);pthread_cond_wait(&cond, &mutex); //醒來的時候,會重新申請鎖!!printf("%s 被叫醒了\n", id);}pthread_mutex_unlock(&mutex);}return nullptr;
}int main(void)
{pthread_t t1, t2, t3, t4;pthread_mutex_init(&mutex, NULL);pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");int cnt = 10;while(true){sleep(5);ticket += cnt;printf("主線程放票嘍, ticket: %d\n", ticket);pthread_cond_signal(&cond);}pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&mutex);
}
5.3 Makefile
bin=testMutex
cc=g++
src=$(wildcard *.cc)
obj=$(src:.cc=.o)$(bin):$(obj)$(cc) -o $@ $^ -lpthread
%.o:%.cc$(cc) -c $< -std=c++17.PHONY:clean
clean:rm -f $(bin) $(obj).PHONY:test
test:echo $(src)echo $(obj)
👥總結
本篇博文對 從互斥原理到C++ RAII封裝實踐 做了一個較為詳細的介紹,不知道對你有沒有幫助呢
覺得博主寫得還不錯的三連支持下吧!會繼續努力的~