一、線程概念
1)線程地址空間
線程與進程共享相同的虛擬地址空間,因此線程在訪問內存時與進程沒有本質的區別。但線程共享和獨占的內存區域有不同的特點,理解這些特性對于正確使用線程至關重要。
1. 線程地址空間的組成
線程的地址空間是進程地址空間的一部分。所有線程共享進程的全局地址空間,同時也有自己的獨立部分。
-
共享部分
-
代碼段:
包含程序的可執行指令,所有線程共享這段內存。通常是只讀的。
-
數據段:
包含初始化的全局變量和靜態變量,所有線程共享。 -
BSS 段:
包含未初始化的全局變量和靜態變量,所有線程共享。 -
堆:
用于動態分配內存(如malloc
或new
),所有線程共享。 -
內存映射區域:
通過mmap
等函數分配的內存,所有線程共享。
-
-
獨立部分
-
棧:
每個線程有自己的獨立棧,存儲局部變量、函數調用幀、返回地址等。 -
線程局部存儲(TLS):
每個線程都有獨立的線程局部存儲空間,用于存儲線程獨占的變量。 -
寄存器上下文:
每個線程有獨立的寄存器,包括程序計數器(PC)、棧指針(SP)等,用于管理線程的執行狀態。
-
2. 地址空間布局
以下是多線程程序典型的虛擬地址空間布局示意圖:
+--------------------------+ High Memory Address
| 內核空間 | <- 僅內核訪問
+--------------------------+
| 棧 (線程 N) | <- 每個線程獨立棧空間
| 棧 (線程 2) |
| 棧 (線程 1) |
+--------------------------+
| 堆 | <- 動態分配的內存 (共享)
+--------------------------+
| 已初始化數據段 (.data) | <- 全局和靜態變量 (共享)
| 未初始化數據段 (.bss) |
+--------------------------+
| 可執行代碼段 (.text) | <- 程序代碼 (共享)
+--------------------------+
| 動態鏈接庫 (共享庫) |
+--------------------------+
| 內存映射區域 | <- mmap 區域 (共享)
+--------------------------+ Low Memory Address
3. 共享與獨立的作用與意義
共享部分
- 全局變量和靜態變量(.data/.bss 段)
- 所有線程共享,因此修改一個線程中的全局變量會影響其他線程。
- 易引發數據競爭,需要使用同步機制(如互斥鎖或信號量)。
- 堆
- 堆是動態分配內存的區域,所有線程共享。
- 動態內存分配(如
malloc
或new
)需要線程安全的實現,如線程安全的分配器。
獨立部分
- 棧
- 每個線程有自己的棧空間,用于存儲局部變量、函數調用幀等。
- 棧空間獨立,線程間的局部變量不會互相干擾。
- 寄存器上下文
- 每個線程有自己的程序計數器、棧指針等,線程的執行狀態獨立。
- 線程局部存儲(TLS)
- 提供每個線程獨占的變量存儲區,適合線程私有數據。
4. 棧的獨立性
棧的獨立性是線程的重要特點。每個線程的棧:
- 在創建線程時由內核分配。
- 棧的大小可以通過
pthread_attr_setstacksize
或 C++11 的線程庫配置。 - 從高地址向低地址擴展。
示例代碼展示線程棧的獨立性:
cpp復制編輯#include <iostream>
#include <thread>void thread_function(int id) {int local_var = id; // 棧上的局部變量std::cout << "Thread " << id << " | Address of local_var: " << &local_var << std::endl;
}int main() {std::thread t1(thread_function, 1);std::thread t2(thread_function, 2);t1.join();t2.join();return 0;
}
輸出示例:
less復制編輯Thread 1 | Address of local_var: 0x7fff5f3f7000
Thread 2 | Address of local_var: 0x7fff5f2f7000
觀察:
- 兩個線程的
local_var
地址不同,說明各線程有獨立的棧空間。
2)同步與互斥
在多線程或多進程環境中,同時操作共享資源或需要協作完成任務是常見需求。而 同步 和 互斥 是解決并發問題的兩種關鍵技術,它們的目標是確保系統的正確性、穩定性和高效性。
1. 同步
同步(Synchronization)是指為了保證多個線程或進程按照正確的邏輯順序執行,協調它們的操作,使得某些事件或條件滿足后,其他線程才能繼續執行。
-
強調協作:線程之間需要等待其他線程完成某些操作,常用于線程之間的依賴關系場景。
可能會阻塞:線程在等待條件滿足時,可能會進入等待狀態,直到相關條件被觸發。
常見機制:
- 條件變量:線程通過條件變量進行等待或通知操作。
- 信號量:控制線程的運行順序或資源計數。
- 屏障(Barrier):確保多個線程在某個階段完成后再進入下一個階段。
常見應用場景:
- 生產者-消費者模型。
- 多線程任務的階段性同步。
2. 互斥
互斥(Mutual Exclusion)是指為了防止多個線程同時訪問共享資源而引發的競爭條件,確保同一時間內只有一個線程能夠訪問資源。
- 強調獨占訪問:保證資源一次只被一個線程訪問,防止并發修改帶來的不一致性。
- 避免數據競爭:通過互斥保護臨界區,確保共享數據的完整性和一致性。
- 可能會阻塞:線程在嘗試獲取鎖失敗時,會被阻塞,直到其他線程釋放鎖。
- 常見機制:
- 互斥鎖(Mutex):實現線程對資源的獨占訪問。
- 讀寫鎖(Read-Write Lock):區分讀操作和寫操作,提高讀操作的并發性。
- 自旋鎖(Spinlock):線程忙等待獲取鎖,適用于短時間鎖定場景。
- 常見應用場景:
- 臨界區保護。
- 文件、內存或數據庫的共享資源訪問。
寄存器是 CPU 的硬件資源,線程在執行時加載共享數據到寄存器,將數據內容拷貝到自身的上下文中獨立操作。這種方式提升了訪問速度,但也引入了數據一致性問題,寄存器只是數據的載體,而不是數據本身。即使兩個線程可以訪問相同的寄存器,其內容可能因上下文切換或不同線程的操作而不同。
鎖操作本身就是被設計成原子的
3. 饑餓
饑餓問題(Starvation)是并發程序設計中的一個常見問題,指的是在多線程或多進程環境中,由于調度策略或資源競爭,某些線程或進程長時間無法獲得所需的資源,導致無法執行或完成任務。這種情況通常發生在某些線程或進程頻繁被其他線程或進程搶占或延遲,造成它們無法得到足夠的執行時間。
-
引起原因:
-
不公平的資源分配:如果資源分配策略不公平,一些線程可能會總是被其他線程阻塞,無法獲得資源。例如,某些線程總是被調度到而其他線程始終得不到執行機會。
-
調度策略問題:在某些調度算法中(如優先級調度),低優先級的線程可能永遠無法獲得 CPU 時間片,因為高優先級的線程總是優先執行。
-
死鎖相關問題:雖然死鎖通常意味著所有線程都被阻塞,但某些情況下,部分線程可能會處于一種長期等待狀態,無法獲得資源,導致它們“饑餓”。
-
不當的鎖或信號量使用:如果一個線程持有鎖時間過長,而其他線程需要這些資源,它們可能長時間無法執行。
-
-
解決方法
- 公平調度算法:
- 輪轉調度(Round-robin scheduling):這種算法確保每個線程都能輪流獲得 CPU 時間片,從而避免某些線程長時間得不到執行。
- 公平鎖(Fair Mutex):保證多個線程公平地訪問共享資源,比如使用“公平互斥鎖”(
pthread_mutex_t
的某些變種可以提供公平性)。 - 優先級反轉避免饑餓:一些實時操作系統(RTOS)采用優先級繼承機制,即當高優先級線程等待低優先級線程釋放鎖時,低優先級線程的優先級會被臨時提升,從而避免低優先級線程一直無法獲得鎖。
- 優先級調整:
- 動態調整優先級:某些系統采用動態優先級調整策略,即低優先級的線程經過一段時間后,優先級會逐步提高,以確保它們最終能獲得執行機會。
- 老化機制(Aging Mechanism):這種機制通過逐步增加線程的優先級來避免其長時間得不到執行,從而避免饑餓。
- 資源限制與控制:
- 對資源進行限制和分配,可以使用信號量、互斥鎖等同步原語來控制資源的訪問順序,避免個別線程在資源競爭中始終無法獲得資源。
- 增加等待時間上限:為了防止某些線程在等待資源時被無限期阻塞,可以設置等待時間的上限(如最大等待次數或最大超時),一旦超過上限,線程就會被重新調度或執行其他任務。
- 優先級公平性:
- 使用優先級公平調度算法,確保所有線程都有機會執行,避免低優先級線程始終得不到執行機會。
- 公平調度算法:
3)可重入與線程安全
1. 可重入
可重入指的是一種程序或函數的特性,即在被中斷后,如果中斷處理程序再次調用該函數,原先的調用不會受到影響,能夠正確地執行并返回預期結果。換句話說,可重入函數是線程安全的,但不僅限于多線程環境。
-
特性
-
不依賴于全局或靜態變量:函數內部使用的所有變量都必須是局部變量,或者通過參數傳遞。
-
不修改共享資源:函數中不能操作或修改共享的全局資源。
-
不調用不可重入的函數:如果一個函數調用了非可重入的函數,則它本身也不可重入。
-
無狀態依賴:函數的行為不依賴于之前的調用狀態。
-
無阻塞操作:函數中不能使用可能引起阻塞的系統調用,例如動態內存分配(
malloc
)或文件I/O操作。
-
-
意義:
- 線程安全:在多線程環境中,多個線程可以并發地調用該函數,而不會發生數據競爭或資源沖突。
- 中斷安全:在中斷處理程序中調用不會破壞原先的執行狀態。
2. 線程安全
線程安全是指多個線程可以并發執行某個函數或操作時,不會由于競爭條件而導致不一致的結果。具體來說,線程安全的函數在多個線程同時調用時,不會因為資源競爭、狀態沖突等問題而導致程序出現錯誤。通常,線程安全涉及對共享資源(如全局變量、靜態變量)的保護。
-
特性
-
鎖機制:線程安全的函數通常會使用鎖(如互斥鎖、讀寫鎖)來保護共享資源,確保在某一時刻只有一個線程能夠訪問資源。
-
無數據競爭:所有線程對共享資源的訪問都會被正確同步,從而避免數據競爭(race condition)。
-
一致性:線程安全的函數保證了多線程操作時數據的一致性,避免不同線程的操作交錯導致結果不可預知。
-
-
線程安全的實現方式:
- 互斥鎖(mutex):使用互斥鎖可以保護對共享資源的訪問,確保每次只有一個線程能夠訪問該資源。
- 原子操作:一些操作(如
atomic
操作)可以保證在多線程環境下的操作是不可中斷的,從而避免了競爭條件。 - 線程局部存儲(TLS):在某些場景中,使用線程局部存儲可以避免線程間的資源競爭,因為每個線程都持有自己的數據副本。
3. 可重入與線程安全
-
關系:
-
可重入是線程安全的一種形式:
-
如果一個函數是可重入的,那么它必定是線程安全的,因為可重入函數的特性使得它能夠在多個線程中獨立運行,不會發生資源沖突和數據競爭。
-
然而,線程安全的函數不一定是可重入的。線程安全的函數可能依賴于全局狀態或者使用鎖來同步資源,這會導致它在多個執行流(如多線程和中斷)中不再保持可重入性。
-
可重入函數一定是線程安全的:
- 由于可重入函數在并發執行時能夠保持數據一致性,因此它必然能夠在多線程環境中保證線程安全。
-
-
線程安全函數不一定是可重入的:
- 如果一個線程安全的函數使用了全局資源或鎖來確保線程同步,那么它就不是可重入的,因為中斷或另一個線程的調用可能會打亂其內部狀態,導致死鎖或其他問題。
-
-
區別:
特性 可重入 線程安全 定義 函數在被中斷后,仍能被其他執行流(如線程)調用,而不會影響執行結果。 多線程環境下,多個線程同時調用函數時,不會發生數據競爭或不一致的結果。 依賴的資源 不依賴全局或靜態變量,所有數據由函數調用者提供。 可能依賴全局或靜態變量,但通過鎖機制或其他方式保證線程間訪問時的一致性。 多線程環境 可重入的函數在多線程中也一定是線程安全的。 線程安全的函數不一定是可重入的。 常見的線程安全措施 使用局部變量、無共享資源、無外部狀態依賴。 使用鎖、原子操作、線程局部存儲(TLS)等方式保證線程間的同步。 執行流 可被多個執行流(包括中斷)多次調用,不受其他執行流影響。 通過同步機制確保多個線程對資源的訪問不會發生沖突。
4) 死鎖
死鎖是多線程或多進程程序中的一個常見問題,指的是兩個或多個線程或進程在執行過程中,由于爭奪資源而導致相互等待,最終無法繼續執行的狀態。
1. 死鎖的四個必要條件
根據 Carson & Hoare 的死鎖四條件,死鎖的發生需要滿足以下四個必要條件:
-
互斥條件(前提):至少有一個資源是不能共享的,即在同一時刻只能由一個線程或進程占用。如果其他線程或進程請求該資源,則必須等待。
-
占有并等待條件(原則):一個線程或進程已經占有了至少一個資源,并且在等待其他線程或進程持有的資源。
-
非搶占條件(原則):資源不能被強行搶占。即線程持有資源時,其他線程不能強制奪回該資源,只能等待該線程釋放資源。
-
循環等待條件(重要條件):存在一個線程(或進程)等待鏈,鏈中的每個線程都在等待下一個線程持有的資源,形成一個環狀的等待鏈。
2. 死鎖的避免
死鎖避免是通過動態地分析系統的資源分配情況,并采取適當的策略,確保死鎖不會發生。最常見的策略有:
-
資源請求的順序一致:
為每個資源分配一個編號,線程在請求資源時,按照從小到大的順序請求資源。這樣避免了形成循環等待的條件。 -
銀行家算法(Banker’s Algorithm):
銀行家算法是一種避免死鎖的資源分配算法。在分配資源之前,先判斷分配后是否會進入一個安全狀態。如果分配后會導致死鎖,就不分配資源,直到系統進入安全狀態。 -
避免占有并等待:
線程在請求資源時,要求一次性申請所有需要的資源,而不是逐個請求資源。這樣可以避免一個線程持有部分資源并等待其他資源的情況。 -
避免循環等待:
通過對資源進行排序,確保系統中不會形成循環等待。例如,線程在請求資源時,必須按照資源的順序來請求資源,避免形成循環等待。
5)信號量
信號量是多線程和多進程同步中的一種機制,用于控制多個線程或進程對共享資源的訪問。信號量是一種計數器,它用來控制對共享資源的訪問數量。
1. 信號量的基本概念
信號量可以視為一個維護共享資源數量的整數,本質就是一個計數器,表示當前系統中可以訪問該資源的線程數量。信號量的常見類型有兩種:
- 計數信號量(Counting Semaphore):允許任意數量的線程訪問共享資源。它的值可以是任意非負整數,表示當前可用的資源數量。常用于控制多個資源的并發訪問。
- 二進制信號量(Binary Semaphore):是一個特殊的計數信號量,只允許值為
0
或1
,相當于一個互斥鎖。常用于二進制狀態的同步,如資源的占用與釋放。
2. 信號量的基本操作
信號量通常有兩種基本操作:
- P操作(或稱等待操作、減操作):
P
操作會使信號量的值減一。當信號量的值大于0時,線程會繼續執行;如果信號量的值為0,線程將被阻塞,直到其他線程執行 V 操作增加信號量的值。
- V操作(或稱釋放操作、加操作):
V
操作會使信號量的值加一。當信號量的值增加時,如果有線程被阻塞在P
操作中,系統會喚醒一個線程繼續執行。
3. 信號量的應用場景
信號量主要用于以下幾種場景:
- 資源訪問控制:當有多個線程或進程需要共享有限的資源時,可以使用信號量來限制并發訪問的數量。例如,數據庫連接池、線程池等都可以使用信號量來限制并發連接數。
- 同步互斥:信號量可以用于線程之間的同步,確保不同線程按照一定順序執行。例如,通過信號量保證線程按順序執行或等待某些條件發生。
- 生產者-消費者問題:信號量通常用于解決生產者-消費者模型中的同步問題。生產者線程和消費者線程之間通過信號量協調共享緩沖區的使用。
- 任務調度:當多個線程需要處理多個任務并且每個任務可能需要不同的資源時,信號量可以協調任務調度,保證有限資源不會被多個任務同時占用。
4. 信號量與同步操作
進程同步接口(如互斥鎖和條件變量)更適合于保護資源的獨占訪問和線程間同步,而信號量則用于控制對資源的并發訪問,適合于需要控制多個資源的并發訪問的場景。
特性 | 進程同步接口 | 信號量 |
---|---|---|
用途 | 主要用于資源的互斥訪問和線程同步。 | 主要用于控制并發訪問資源的數量。 |
操作原語 | pthread_mutex_lock() , pthread_cond_wait() , pthread_cond_signal() 等 | sem_wait() , sem_post() 等 |
適用場景 | 保護共享資源、線程間的協調和通信、條件等待等。 | 控制多個線程對共享資源的訪問,避免過多線程并發操作資源。 |
靈活性 | 適用于訪問單一共享資源的同步控制。 | 適用于多個資源的訪問控制,支持并發控制。 |
簡單性 | 操作簡單,適用于資源保護。 | 提供更多的控制選項,適合多個資源的并發訪問。 |
二、線程庫
1)線程控制
1. 進程創建
-
pthread_creat()
:用于創建新線程的函數。它允許程序在同一個進程中并發執行多個任務。int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg)
#include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h>// 線程函數 void *thread_function(void *arg) {int *num = (int *)arg; // 將參數轉換為適當類型printf("Thread %d is running\n", *num);sleep(1); // 模擬任務printf("Thread %d has finished\n", *num);return NULL; }int main() {pthread_t threads[5];int thread_args[5];for (int i = 0; i < 5; i++) {thread_args[i] = i + 1; // 參數傳遞if (pthread_create(&threads[i], NULL, thread_function, &thread_args[i]) != 0) {perror("Failed to create thread");return 1;}}// 等待線程結束for (int i = 0; i < 5; i++) {pthread_join(threads[i], NULL);}printf("All threads have completed\n");return 0; }
thread
:用于存儲創建線程的線程 ID,系統自動分配。(輸出型參數)attr
:用于設置線程的屬性(例如是否為分離線程、棧大小等)。NULL
:使用默認屬性。(一般都用該設置)
start_routine
:線程執行的函數。arg
:傳遞給start_routine
函數的參數。- 返回值:
0
:成功創建線程。- 非
0
:返回錯誤碼,表示線程創建失敗。
pthread_create
本身 不是直接的系統調用,編譯時需要自己鏈接該庫:-lpthread
。- 返回的線程與系統中的
LWP
并不是同一個東西。、
2. 線程等待
-
pthread_join()
:用于阻塞調用線程,直到指定的目標線程終止為止。通常用于等待線程的完成,并獲取線程的返回值。int pthread_join(pthread_t thread, void **retval)
#include <pthread.h> #include <stdio.h>void *thread_function(void *arg) {printf("Thread is running with arg: %d\n", *(int *)arg);pthread_exit((void *)42); // 返回值 }int main() {pthread_t thread;int arg = 10;void *retval;// 創建線程if (pthread_create(&thread, NULL, thread_function, &arg) != 0) {perror("Failed to create thread");return 1;}// 等待線程完成if (pthread_join(thread, &retval) != 0) {perror("Failed to join thread");return 1;}printf("Thread completed with return value: %ld\n", (long)retval);return 0; }
thread
:要等待的線程ID(pthread_create
的第一個參數返回的值)。retval
:用于存儲目標線程的退出返回值。- 如果目標線程使用
pthread_exit
返回了某個值,該值會通過此參數返回給調用線程。 NULL
:若不需要返回值。
- 如果目標線程使用
- 返回值:成功返回
0
,失敗返回錯誤碼。
3. 線程退出
-
pthread_exit()
:用于讓當前線程安全地退出,同時向其他線程傳遞返回值。void pthread_exit(void *retval)
pthread_exit((void *)42);
retval
:調用線程的退出狀態,通常用來傳遞返回值給其他線程。如果該線程被其他線程pthread_join
,則retval
會通過pthread_join
的第二個參數返回。
4. 線程取消
-
pthread_cancel()
:用于向目標線程發送取消請求,要求其終止。目標線程是否響應取決于其取消狀態和取消類型。int pthread_cancel(pthread_t thread)
pthread_create(&thread, NULL, thread_function, NULL) pthread_cancel(thread)
thread
:要發送取消請求的目標線程ID。- 返回值:成功返回
0
,失敗返回錯誤碼。
2)互斥鎖
1. 初始化互斥鎖
-
pthread_mutex_init()
: 用于初始化互斥鎖(pthread_mutex_t
類型)的函數。它允許開發者創建一個互斥鎖,并為鎖設置指定的屬性。int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr)
mutex
:指向要初始化的互斥鎖對象的指針(pthread_mutex_t
類型)。(通常是一個未初始化的全局或靜態變量)attr
:指向互斥鎖屬性對象的指針(pthread_mutexattr_t
類型)。(如果不需要特定的屬性,可以傳入NULL
,此時互斥鎖將使用默認屬性。)- 返回值:成功返回
0
,失敗返回錯誤碼。
靜態初始化
如果互斥鎖在程序中是全局變量,可以通過靜態初始化代替動態初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
靜態初始化確實不需要顯式的初始化(
init
)和銷毀(destroy
)操作。
2. 加鎖
-
pthread_mutex_lock()
:是用于加鎖互斥鎖的函數。當線程需要進入一個臨界區時,可以通過調用這個函數加鎖,從而防止其他線程同時訪問共享資源。int pthread_mutex_lock(pthread_mutex_t *mutex)
mutex
:指向需要加鎖的互斥鎖對象(pthread_mutex_t
類型)的指針。- 返回值:成功返回
0
,失敗返回錯誤碼。
- 加鎖的本質是用時間換安全。因此需要保證臨界區代碼越少越好。
-
pthread_mutex_trylock()
:與pthread_mutex_lock
不同,pthread_mutex_trylock
不會阻塞當前線程。如果互斥鎖已經被其他線程持有,pthread_mutex_trylock
會立即返回,而不是讓當前線程等待。int pthread_mutex_trylock(pthread_mutex_t *mutex)
mutex
:指向互斥鎖對象(pthread_mutex_t
類型)的指針,表示要嘗試獲取的鎖。- 返回值:成功返回
0
,失敗返回錯誤碼。
3. 解鎖
-
pthread_mutex_unlock()
:用于解鎖互斥鎖的函數。當線程完成對共享資源的操作后,調用該函數釋放互斥鎖,以便其他線程可以獲取鎖并訪問臨界區。int pthread_mutex_unlock(pthread_mutex_t *mutex)
mutex
:指向需要解鎖的互斥鎖對象(pthread_mutex_t
類型)的指針。- 返回值:成功返回
0
,失敗返回錯誤碼。
4. 銷毀鎖
-
pthread_mutex_destroy()
:用于銷毀一個已經初始化的互斥鎖(pthread_mutex_t
類型)的函數。在不再需要使用互斥鎖時,應調用該函數釋放分配的資源。int pthread_mutex_destroy(pthread_mutex_t *mutex)
mutex
:指向需要銷毀的互斥鎖對象的指針(pthread_mutex_t
類型)。- 返回值:成功返回
0
,失敗返回錯誤碼。
#include <pthread.h>
#include <stdio.h>pthread_mutex_t mutex;void *thread_function(void *arg) {pthread_mutex_lock(&mutex); // 加鎖printf("Thread %ld: Entered critical section\n", (long)arg);// 模擬臨界區操作sleep(1);pthread_mutex_unlock(&mutex); // 解鎖printf("Thread %ld: Exited critical section\n", (long)arg);return NULL;
}int main() {pthread_t t1, t2;pthread_mutex_init(&mutex, NULL);pthread_create(&t1, NULL, thread_function, (void *)1);pthread_create(&t2, NULL, thread_function, (void *)2);pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_mutex_destroy(&mutex);return 0;
}
3)同步
1. 條件變量(pthread_mutex_t
)
- 條件變量用于線程之間的通信和協調,它通常與互斥鎖(
pthread_mutex_t
)一起使用。條件變量允許線程在某些條件滿足時進行同步,解決了線程之間的等待和通知問題。 - 條件變量的主要作用是使得線程可以在某個條件不滿足時等待,直到條件滿足時再被喚醒執行。它允許線程在等待某個特定條件時不占用 CPU 資源(即避免忙等待),直到收到其他線程的通知。
2. 等待條件變量
-
pthread_cond_wait()
:用于使當前線程在條件變量上等待,并釋放互斥鎖,直到其他線程通過pthread_cond_signal()
或pthread_cond_broadcast()
喚醒它。int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
-
cond
:指向條件變量對象(pthread_cond_t
類型)的指針,表示要等待的條件變量。 -
mutex
:指向互斥鎖對象(pthread_mutex_t
類型)的指針,表示在等待期間保護的資源。 -
返回值:成功返回
0
,失敗返回錯誤碼。
-
3. 喚醒條件變量
-
pthread_cond_signal()
:用于喚醒一個等待在指定條件變量上的線程。只有一個等待線程會被喚醒。如果沒有線程在等待,調用該函數不會有任何效果。int pthread_cond_signal(pthread_cond_t *cond)
cond
:指向條件變量對象(pthread_cond_t
類型)的指針,表示要發送信號的條件變量。- 返回值:成功返回
0
,失敗返回錯誤碼。
-
pthread_cond_broadcast()
:用于喚醒所有等待在指定條件變量上的線程。int pthread_cond_broadcast(pthread_cond_t *cond)
cond
:指向條件變量對象(pthread_cond_t
類型)的指針,表示要發送信號的條件變量。- 返回值:成功返回
0
,失敗返回錯誤碼。
pthread_cond_signal()
調用時會釋放鎖,返回時會持有鎖。
4)信號量
1. 初始化信號量
-
sem_init()
:用于初始化一個信號量。信號量可以用于多線程或多進程間的同步,sem_init
設置了信號量的初始值。int sem_init(sem_t *sem, int pshared, unsigned int value)
sem_t sem; sem_init(&sem, 0, 1); // 初始化一個值為 1 的信號量
sem
:指向信號量的指針。pshared
:指定信號量的共享范圍。0
表示信號量在線程間共享,1
表示信號量在進程間共享。value
:初始化信號量的值,通常表示可用資源的數量。- 返回值:成功返回
0
,失敗返回錯誤碼。
2. 等待信號量
-
sem_wait()
:信號量的等待操作(P操作)。如果信號量的值大于 0,線程將繼續執行并將信號量減 1;如果信號量的值為 0,線程將被阻塞,直到信號量的值大于 0。int sem_wait(sem_t *sem)
sem_t sem; sem_wait(&sem); // 等待空槽位
sem
:指向信號量的指針。- 返回值:成功返回
0
,失敗返回錯誤碼。
3. 釋放信號量
-
sem_post()
:信號量的釋放操作(V操作)。將信號量的值增加 1,如果有線程因sem_wait()
被阻塞,則喚醒一個線程。int sem_post(sem_t *sem);
sem_t sem; sem_post(&sem); // 通知消費者緩沖區有數據
sem
:指向信號量的指針。- 返回值:成功返回
0
,失敗返回錯誤碼。
4. 銷毀信號量
-
sem_destroy()
:銷毀一個已初始化的信號量,釋放資源。銷毀前信號量應不再被任何線程使用。int sem_destroy(sem_t *sem)
sem_destroy(&sem); // 銷毀信號量
sem
:指向信號量的指針。- 返回值:成功返回
0
,失敗返回錯誤碼。
三、多線程問題
1)生產者-消費者模型
生產者-消費者模型(Producer-Consumer Model)是一種經典的多線程問題,主要用于描述多個線程如何共享資源并進行數據交換。在這個模型中,有兩個主要的角色:生產者和消費者,它們通過某種形式的緩沖區(通常是隊列)交換數據。
1. 模型背景與概述
生產者-消費者模型是多線程編程中的經典同步問題,它的目標是協調多個線程的操作,確保生產者和消費者之間的交互不會發生沖突。具體來說:
- 生產者:生產者線程負責產生數據,并將數據放入共享緩沖區或隊列中。
- 消費者:消費者線程負責從共享緩沖區或隊列中取出數據并消費。
緩沖區的大小是有限的,因此生產者和消費者之間需要協調:如果緩沖區已滿,生產者需要等待;如果緩沖區為空,消費者需要等待。
2. 問題的關鍵點
-
一種交易場所:
- 特定結構的內存空間
-
兩種角色:
- 生產者
- 消費者
-
三種關系:
- 生產者-生產者:互斥
- 消費者-消費者:互斥
- 生產者-消費者:互斥,同步
3. 優點
-
緩解不平衡問題
在許多應用中,生產者和消費者的工作速度往往是不一樣的。生產者可能產生數據的速度比消費者消費數據的速度要快,或者反過來。通過生產者-消費者模型,能夠有效地解決這種忙閑不均的問題。
-
生產者忙,消費者閑:當生產者的速度遠大于消費者時,生產者會將生成的數據放入緩沖區,消費者可以在其有空時從緩沖區中取出數據進行消費。
例如,在視頻流處理中,生產者(視頻流產生器)可能生成數據的速度遠高于消費者(播放器),緩沖區可以容納大量數據,確保播放過程不中斷。
-
消費者忙,生產者閑:如果消費者處理數據的速度超過生產者,則消費者可能會等待生產者生產更多的數據。在這種情況下,生產者在有能力時可以開始生產數據,而消費者則能夠快速地消費這些數據。
-
緩沖區的作用:緩沖區作為生產者和消費者之間的中介,平衡了生產者和消費者之間的不平衡問題。當生產者忙時,數據被放入緩沖區;當消費者忙時,數據等待在緩沖區中,直到消費者有空。
-
-
解耦生產與消費
生產者-消費者模型通過解耦生產和消費,使得兩者的工作可以獨立進行,這帶來了更高的靈活性和擴展性:
- 生產與消費的獨立性:生產者和消費者在同一個系統中各自獨立地運行。生產者只關心如何生產數據,消費者只關心如何消費數據,二者之間沒有直接的依賴。它們通過共享的緩沖區進行交互,緩解了它們之間的緊密耦合。
- 靈活的負載調度:由于生產者和消費者是解耦的,當生產者和消費者的工作負載發生變化時,系統能夠根據需要進行動態調整。例如,可以增加更多的消費者線程以提高消費速率,或者增加生產者線程以提高生產速度,而不會影響另一方的操作。
- 擴展性:在需要提升系統的吞吐量時,可以獨立地增加生產者或消費者的數量,而無需改變系統的結構或流程。通過增加生產者,可以提升數據生成的速率;通過增加消費者,可以提升數據消費的速率。這樣系統的擴展變得更加靈活和高效。
- 異步處理:生產者和消費者不需要等待對方的操作完成,允許它們在不同的時間點進行工作。這種異步的處理方式使得系統能夠高效地利用資源,減少等待時間,提高整體性能。
-
生產者與消費者的協調
雖然生產者和消費者解耦,互不直接依賴,但它們之間仍然需要協調和同步,以確保數據的正確處理。使用 條件變量 和 互斥鎖 等同步機制能夠協調生產者與消費者的工作:
- 生產者等待條件:生產者在緩沖區已滿時會等待,直到消費者消費了數據騰出空間。生產者通過條件變量(
pthread_cond_t
)來等待并被喚醒。 - 消費者等待條件:消費者在緩沖區為空時會等待,直到生產者生產了數據填充緩沖區。消費者也通過條件變量來等待并被喚醒。
- 生產者等待條件:生產者在緩沖區已滿時會等待,直到消費者消費了數據騰出空間。生產者通過條件變量(
生產者-消費者模型(互斥鎖與條件變量):
#include <pthread.h> #include <stdio.h> #include <unistd.h>#define MAX_ITEMS 10 // 緩沖區的最大容量 int buffer[MAX_ITEMS]; // 緩沖區 int count = 0; // 當前緩沖區中的項目數pthread_mutex_t mutex; // 互斥鎖,保護對緩沖區的訪問 pthread_cond_t cond_full; // 條件變量,表示緩沖區已滿 pthread_cond_t cond_empty; // 條件變量,表示緩沖區為空// 生產者線程 void* producer(void* arg) {for (int i = 0; i < 20; i++) {pthread_mutex_lock(&mutex);// 如果緩沖區已滿,等待while (count == MAX_ITEMS) {pthread_cond_wait(&cond_full, &mutex);}// 生產一個新項目buffer[count] = i;count++;printf("Produced: %d\n", i);// 通知消費者緩沖區有數據可以消費pthread_cond_signal(&cond_empty);pthread_mutex_unlock(&mutex);sleep(1); // 模擬生產過程}return NULL; }// 消費者線程 void* consumer(void* arg) {for (int i = 0; i < 20; i++) {pthread_mutex_lock(&mutex);// 如果緩沖區為空,等待while (count == 0) {pthread_cond_wait(&cond_empty, &mutex);}// 消費一個項目int item = buffer[count - 1];count--;printf("Consumed: %d\n", item);// 通知生產者緩沖區有空間可以生產pthread_cond_signal(&cond_full);pthread_mutex_unlock(&mutex);sleep(2); // 模擬消費過程}return NULL; }int main() {pthread_t prod, cons;pthread_mutex_init(&mutex, NULL);pthread_cond_init(&cond_full, NULL);pthread_cond_init(&cond_empty, NULL);// 創建生產者和消費者線程pthread_create(&prod, NULL, producer, NULL);pthread_create(&cons, NULL, consumer, NULL);// 等待線程結束pthread_join(prod, NULL);pthread_join(cons, NULL);// 銷毀互斥鎖和條件變量pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond_full);pthread_cond_destroy(&cond_empty);return 0; }
生產者線程:
生產者線程通過
pthread_mutex_lock
獲取互斥鎖來操作共享緩沖區,防止其他線程同時訪問緩沖區。如果緩沖區已滿,生產者通過
pthread_cond_wait
等待消費者線程消費數據,直到緩沖區有空間。每次生產完數據后,生產者調用
pthread_cond_signal
喚醒消費者線程,通知其可以消費數據。消費者線程:
消費者線程通過
pthread_mutex_lock
獲取互斥鎖來操作共享緩沖區。如果緩沖區為空,消費者通過
pthread_cond_wait
等待生產者生產數據,直到緩沖區有數據。每次消費完數據后,消費者調用
pthread_cond_signal
喚醒生產者線程,通知其可以繼續生產。條件變量:
pthread_cond_wait
:當條件不滿足時(如緩沖區已滿或為空),線程將阻塞,釋放互斥鎖并進入等待狀態。
pthread_cond_signal
:通知一個等待的線程,喚醒它繼續執行。判斷必須放在鎖里
生產者-消費者模型(信號量):
#include <pthread.h> #include <stdio.h> #include <unistd.h> #include <semaphore.h> // 包含信號量頭文件#define MAX_ITEMS 10 // 緩沖區的最大容量 int buffer[MAX_ITEMS]; // 緩沖區 int count = 0; // 當前緩沖區中的項目數sem_t empty_slots; // 信號量,表示空槽位 sem_t full_slots; // 信號量,表示已占用的槽位 pthread_mutex_t mutex; // 互斥鎖,保護對緩沖區的訪問// 生產者線程 void* producer(void* arg) {for (int i = 0; i < 20; i++) {sem_wait(&empty_slots); // 等待空槽位pthread_mutex_lock(&mutex); // 訪問緩沖區前加鎖// 生產一個新項目buffer[count] = i;count++;printf("Produced: %d\n", i);pthread_mutex_unlock(&mutex); // 解鎖sem_post(&full_slots); // 通知消費者緩沖區有數據sleep(1); // 模擬生產過程}return NULL; }// 消費者線程 void* consumer(void* arg) {for (int i = 0; i < 20; i++) {sem_wait(&full_slots); // 等待已占用槽位pthread_mutex_lock(&mutex); // 訪問緩沖區前加鎖// 消費一個項目int item = buffer[count - 1];count--;printf("Consumed: %d\n", item);pthread_mutex_unlock(&mutex); // 解鎖sem_post(&empty_slots); // 通知生產者緩沖區有空槽位sleep(2); // 模擬消費過程}return NULL; }int main() {pthread_t prod, cons;// 初始化信號量和互斥鎖sem_init(&empty_slots, 0, MAX_ITEMS); // 初始化空槽位信號量sem_init(&full_slots, 0, 0); // 初始化已占用槽位信號量pthread_mutex_init(&mutex, NULL);// 創建生產者和消費者線程pthread_create(&prod, NULL, producer, NULL);pthread_create(&cons, NULL, consumer, NULL);// 等待線程結束pthread_join(prod, NULL);pthread_join(cons, NULL);// 銷毀信號量和互斥鎖sem_destroy(&empty_slots);sem_destroy(&full_slots);pthread_mutex_destroy(&mutex);return 0; }
2)偽喚醒狀態
偽喚醒是多線程編程中可能遇到的一個問題,尤其是在使用條件變量(pthread_cond_t
)時。偽喚醒指的是線程在沒有收到信號(如 pthread_cond_signal
或 pthread_cond_broadcast
)的情況下被喚醒,即條件變量的等待線程被意外喚醒,但實際上條件并沒有滿足。
1. 偽喚醒的原因
偽喚醒通常是由于操作系統調度、線程的內部狀態更新或底層線程庫的實現細節等原因引起的。具體原因因平臺和線程庫實現的不同而有所差異,但通常來說:
- 操作系統的線程調度器:可能會喚醒一個等待線程,而沒有通知線程它應該繼續執行。這是因為操作系統將線程從“等待”狀態轉移到“就緒”狀態,但并沒有確定此時條件已經滿足,因此線程被喚醒時可能并不應繼續執行。
- 條件變量的實現:某些條件變量的實現可能會導致線程在一些邊界情況或時機上被喚醒,而此時它們并不應該繼續執行。比如,當一個線程等待某個條件時,雖然條件沒有滿足,但它依然會被操作系統調度器喚醒。
- 并發環境的復雜性:線程在等待時,可能會因為某些內部的同步機制或調度優先級的變化,產生意外的喚醒,這些喚醒并不總是因為條件已滿足,而是由于線程調度機制的某些策略。
2. 偽喚醒的影響
如果線程在沒有滿足條件的情況下被喚醒,它可能會繼續執行錯誤的代碼,從而引發不一致的行為、邏輯錯誤或資源浪費。
- 程序邏輯錯誤:當線程被喚醒時,它可能沒有完成原本等待的條件檢查,導致執行錯誤的操作。比如,在生產者-消費者問題中,消費者線程可能會在緩沖區為空時被偽喚醒,這時它會嘗試消費數據,導致錯誤或崩潰。
- 死鎖或無效循環:偽喚醒可能導致線程進入錯誤的狀態,甚至可能陷入死鎖或無效循環。例如,如果消費者線程被偽喚醒,但緩沖區依然為空,它可能會繼續等待并反復調用等待操作,導致不必要的資源浪費和死鎖風險。
- 性能浪費:偽喚醒會導致線程無意義地執行不必要的操作,從而浪費 CPU 資源和系統資源,特別是在高并發環境下,可能會顯著影響程序性能。
- 基于偽喚醒狀態,需要在喚醒判斷時使用while循環判斷。
3)線程池
線程池是一種用來管理和復用線程的設計模式,它通常用于處理大量短時間的任務。在這種模式下,線程池內會維護一組預先創建的線程,任務提交給線程池后由線程池中的線程處理。線程池避免了每次任務執行時都需要創建和銷毀線程的開銷,提高了性能。
1. 線程池的工作原理
線程池的基本工作原理可以分為以下幾個步驟:
-
初始化線程池:線程池在創建時會初始化一個線程池的大小(即線程數量),這些線程通常會在后臺一直存在,等待任務到來。
-
提交任務:當有任務需要執行時,任務被提交到線程池中。任務可以是函數、對象或其他類型的可執行單元。
-
線程處理任務:線程池中的空閑線程從任務隊列中獲取任務并執行。執行完任務后,線程返回線程池,繼續等待下一個任務。
-
任務完成:任務完成后,線程池中的線程繼續等待任務的到來,直到線程池銷毀或不再需要工作。
2. 線程池的優勢
- 減少線程創建銷毀的開銷:每次創建和銷毀線程是非常昂貴的操作。使用線程池可以重用已有的線程,減少創建和銷毀線程的開銷。
- 提高響應速度:當有任務到來時,線程池中的線程可以立即開始執行任務,避免了等待線程創建的時間。
- 線程復用:通過線程池的管理,系統可以控制線程的數量,避免過多線程導致的資源競爭和系統過載。
- 任務調度優化:線程池可以根據系統負載和任務優先級進行調度,合理分配系統資源,提高任務處理效率。
3. 線程池的組成部分
一個完整的線程池通常包含以下幾個部分:
- 工作線程:線程池中的線程,負責從任務隊列中取出任務并執行。
- 任務隊列:存儲待處理任務的隊列,通常采用線程安全的數據結構。
- 任務調度器:線程池中的調度模塊,負責調度空閑線程去執行任務。
- 線程池管理器:負責線程池的創建、銷毀和線程的管理。
4. 線程池的設計模式
線程池的設計模式常見的有以下幾種類型:
-
固定大小的線程池:線程池中的線程數是固定的,不會動態調整。適用于任務量相對穩定且需要固定資源的場景。
-
可伸縮的線程池:線程池會根據任務的數量動態調整線程數,增加線程數以應對更多任務,減少線程數以節約資源。
-
單線程線程池:線程池中只有一個線程,適用于任務必須串行執行的場景。
-
緩存線程池:當任務量較少時,線程池中的線程數量可能為零,只有在任務到來時,線程池才會創建新線程。
4)其他鎖
1. 悲觀鎖
悲觀鎖假設在并發環境下,數據在讀取和更新過程中很可能會發生沖突,因此在每次操作數據之前,都主動加鎖以確保數據的安全。主要特點包括:
-
主動加鎖:每次取數據或更新數據前,都會先對數據加鎖(如讀鎖、寫鎖、行鎖等),確保操作期間數據不會被其他線程修改。
-
阻塞機制:當其他線程試圖訪問被悲觀鎖保護的數據時,因鎖已加持,其他線程會被阻塞掛起,直到鎖被釋放。
-
適用場景:適用于寫操作頻繁、數據競爭激烈的場景,因為它通過加鎖機制保證了數據的一致性和正確性。
示例應用:在數據庫中,為了保證數據在更新時不會發生沖突,通常使用悲觀鎖來防止其他事務同時修改數據。
2. 樂觀鎖
樂觀鎖在讀取數據時不會加鎖,而是假設數據不會發生沖突。在更新數據前,再判斷在此期間數據是否被其他線程修改過。如果發生了修改,則更新操作會失敗,通常需要重試。樂觀鎖主要采用兩種方式:
- 版本號機制:在數據中維護一個版本號,每次讀取數據時,同時讀取版本號。在更新數據時,檢查當前數據的版本號是否與讀取時相同;若相同,則更新數據并遞增版本號,否則更新失敗,需要重試。
- CAS 操作(Compare-And-Swap):CAS 是一種原子操作,它會將內存中的當前值與預期值進行比較,如果相等,則將新值寫入內存;否則更新失敗。CAS 操作通常以自旋的方式進行重試,直到成功為止。
優點:
- 避免了傳統鎖的阻塞開銷,適用于沖突較少的場景。
- 能夠提高并發性能,因為線程在大部分情況下不需要加鎖。
缺點:
- 當并發沖突頻繁時,會導致大量重試,自旋浪費 CPU 資源。
- 實現較為復雜,特別是在數據一致性方面需要精細控制。
3. 自旋鎖(Spin Lock)
自旋鎖是一種特殊的鎖,它在獲取鎖失敗時不會掛起線程,而是讓線程在一個循環中不斷“忙等待”,直到鎖被釋放。自旋鎖的特點包括:
-
忙等待:線程在等待鎖時持續循環檢查鎖的狀態,而不進入阻塞狀態。
-
適用于短臨界區:當臨界區執行時間很短時,自旋鎖可以避免線程掛起和喚醒所帶來的上下文切換開銷,從而提高性能。
-
缺點:如果持鎖時間較長,忙等待會浪費大量 CPU 時間,并可能導致資源浪費。
-
應用場景:在多核系統中,對于非常短小且頻繁的臨界區,自旋鎖可以提高并發性能。
4. 公平鎖與非公平鎖
在鎖的設計中,公平性是一個重要考量。鎖的公平性決定了多個線程爭奪鎖時,是否按照請求的順序依次獲得鎖。
- 公平鎖(Fair Lock):
- 順序性:公平鎖按照線程請求鎖的順序分配鎖,保證先請求的線程先獲得鎖。
- 優點:避免了線程饑餓現象,每個線程都有機會獲得鎖。
- 缺點:實現公平性通常需要額外的調度開銷,在高競爭情況下可能降低吞吐量。
- 非公平鎖(Unfair Lock):
- 搶占性:非公平鎖允許后請求的線程在某些情況下“插隊”獲取鎖,未必按照嚴格的順序分配。
- 優點:通常具有更高的吞吐量和更低的延遲,因為它允許更高效地利用系統資源。
- 缺點:可能導致部分線程長時間等待(饑餓問題),因為鎖的分配沒有嚴格的順序保障。
5. 讀寫鎖
讀寫鎖(Read-Write Lock)是一種允許多個線程并行讀取共享資源,但在寫操作時,只允許一個線程修改共享資源的同步機制。讀寫鎖通過區分讀操作和寫操作來提高系統的并發性,尤其在讀操作遠多于寫操作的情況下,能夠顯著提高性能。
-
基本原理
-
讀模式:多個線程可以同時讀數據,只要沒有線程在進行寫操作。即,多個線程可以同時擁有讀鎖。
-
寫模式:寫操作是獨占的,只有一個線程可以對共享資源進行寫操作,并且在寫鎖持有期間,其他線程無法進行任何讀操作或寫操作。
-
-
工作方式
- 多個線程可以同時持有讀鎖:當沒有線程在寫數據時,多個線程可以并行地讀取數據,這能夠提高系統的讀操作性能。
- 寫鎖是互斥的:在任何線程持有寫鎖時,其他線程不能獲得讀鎖或寫鎖。寫鎖確保數據的一致性和完整性。
5)STL、智能指針與線程安全
在現代 C++ 編程中,標準模板庫(STL)、智能指針和線程安全是重要的概念,尤其是在多線程環境下,它們共同決定了代碼的健壯性、性能以及安全性。下面,我們來討論一下這三者之間的關系以及它們如何影響多線程編程。
1. STL與線程安全
STL(Standard Template Library)是 C++ 標準庫中的一部分,提供了一系列常用的容器、算法和迭代器等。它的設計初衷是提供一個高效、可重用的編程工具。然而,STL本身并不保證線程安全。換句話說,在多線程環境中,多個線程同時訪問或修改同一個 STL 容器時,會導致數據競爭和不可預期的行為。
-
STL中的線程安全問題
-
不支持并發修改:多個線程同時對同一個 STL 容器進行修改(如插入、刪除元素等)時,會導致數據損壞。為了保證線程安全,需要顯式地使用鎖(如互斥鎖)來同步對容器的訪問。
-
只支持線程安全的迭代:在多個線程對同一個容器進行只讀操作時(無修改),大多數 STL 容器是安全的。然而,如果一個線程正在修改容器,而另一個線程正在讀取,仍然可能發生競態條件。
-
并發容器:C++11 引入了一些線程安全的容器,如
std::vector
和std::list
等,并沒有內置的并發安全設計,但可以通過加鎖來確保線程安全。C++17 中引入了更高效的并發數據結構,如std::shared_mutex
來實現共享鎖。
-
-
線程安全的使用方式
為了讓 STL 容器在多線程環境中安全使用,通常采取以下策略:
-
使用互斥鎖(
std::mutex
)保護容器:所有訪問容器的操作(無論是讀取還是修改)都必須加鎖,確保每次只有一個線程能操作容器。 -
使用讀寫鎖:在大多數情況下,如果只有少量線程需要修改容器而大多數線程只是讀取容器,可以使用
std::shared_mutex
或std::shared_lock
來提高性能。
-
2. 智能指針與線程安全
智能指針(std::unique_ptr
、std::shared_ptr
、std::weak_ptr
)是 C++11 引入的用于自動管理動態分配內存的工具。它們通過 RAII(資源獲取即初始化)機制幫助開發者減少內存泄漏、空懸指針等問題的出現。智能指針本身有著一定的線程安全特性,但也存在一些需要注意的問題。
-
std::unique_ptr
:- 線程不安全:
std::unique_ptr
是一個獨占所有權的智能指針,意味著在同一時刻,只有一個unique_ptr
擁有對對象的所有權。因此,std::unique_ptr
本身是線程不安全的,不能被多個線程共享。 - 正確的做法:如果需要在多線程環境中共享
std::unique_ptr
,可以使用std::move
轉移所有權,但只能在同一個線程中操作一次,避免多線程同時訪問。
- 線程不安全:
-
std::shared_ptr
:-
線程安全的引用計數:
std::shared_ptr
是一個智能指針,允許多個shared_ptr
實例共享同一個對象。它使用引用計數來管理對象的生命周期,C++ 標準保證引用計數的更新是線程安全的。 -
線程安全的限制:雖然引用計數本身是線程安全的,但多個線程同時訪問同一個對象時,若對象本身不是線程安全的,就可能會引發數據競爭問題。
std::shared_ptr
本身并不保證對象的線程安全,必須保證對象在多個線程中安全訪問。
-
-
std::weak_ptr
:- 不具有所有權:
std::weak_ptr
是一個弱引用,不會影響對象的引用計數,因此不需要考慮線程安全問題。但是,std::weak_ptr
的lock()
方法返回一個shared_ptr
,它需要保證shared_ptr
的線程安全。
- 不具有所有權:
3. 智能指針與線程安全的結合
-
避免共享
unique_ptr
:std::unique_ptr
不應在多個線程間共享。如果需要在多個線程間傳遞對象的所有權,應通過std::move
轉移所有權。 -
使用
shared_ptr
時的保護:std::shared_ptr
的引用計數是線程安全的,但如果多個線程訪問共享的對象本身,必須保證對象的訪問是線程安全的。可以通過加鎖或使用其他線程同步機制來確保對象的線程安全。 -
避免同時使用多個智能指針:盡量避免在多個線程中使用同一個對象的多個
shared_ptr
,因為雖然引用計數是線程安全的,但多個線程操作同一對象時,仍然可能導致數據競爭。
6)單例模式
單例模式(Singleton Pattern)是一種常見的設計模式,其主要目的是確保一個類只有一個實例,并且提供一個全局訪問點來獲取該實例。單例模式常用于系統中需要唯一實例的場景,例如數據庫連接、配置管理器等。
1. 單例模式的特點
- 唯一性:單例類只能有一個實例。
- 全局訪問:提供一個全局的訪問點來獲取實例。
- 懶加載:通常單例實例在第一次使用時才被創建,而不是在程序啟動時創建。
- 線程安全:在多線程環境下,確保實例的創建是線程安全的。
2. 單例模式的優缺點
-
優點:
-
節省內存:通過確保系統中只存在一個實例,避免了多次創建和銷毀相同對象所帶來的內存浪費。
-
全局訪問點:單例模式提供了一個全局訪問點,可以方便地獲取到唯一的實例,避免了全局變量的使用。
-
惰性初始化:單例實例在第一次使用時才創建,可以有效節省啟動時的資源消耗。
-
-
缺點:
-
隱藏依賴關系:由于單例模式提供了全局訪問點,這可能導致代碼中隱式的依賴關系,難以進行測試和維護。
-
難以擴展:如果需要擴展單例類或實例化多個實例,單例模式就不再適用。
-
線程安全問題:在多線程環境下,單例實例的創建需要額外的線程同步機制,否則可能會出現競爭條件和多次創建實例的問題。
-
3. 單例模式的使用場景
- 配置管理器:例如,程序讀取配置信息時,只需要一個全局配置管理實例,避免頻繁讀取和修改配置。
- 日志系統:程序中的日志記錄通常使用單例模式來保證日志記錄的唯一性。
- 數據庫連接池:數據庫連接池通常使用單例模式來保證池中數據庫連接的唯一性和共享。
- 線程池:線程池的管理通常也會使用單例模式,確保全局只有一個線程池實例。
4. 單例模式設計方式
-
餓漢式
餓漢式單例模式在類加載時就創建了單例對象,而不是等到第一次使用時才創建。這個方法簡單且直接,通常不需要考慮線程安全問題,因為類加載的過程是線程安全的。
template <typename T> class Singleton { static T data; public: static T* GetInstance() { return &data; } };
- 實例化時機:在類加載時創建實例。
- 線程安全:因為類加載過程是線程安全的,實例化過程無需加鎖。
- 缺點:即使實例可能不會被使用,類加載時就已經創建了實例,這會浪費資源。
-
懶漢式
懶漢式單例模式采用延遲實例化的策略,只有在第一次使用單例對象時才創建它。這種方法在創建對象之前不會占用任何資源,適合資源消耗較大的對象。
template <typename T> class Singleton { static T* inst; public: static T* GetInstance() { if (inst == NULL) { inst = new T(); } return inst; } };
- 實例化時機:只有在需要使用實例時才進行創建。
- 線程安全問題:在多線程環境下,需要特別注意線程安全問題。