線程同步是指多個線程協調工作,以便在共享資源的訪問和操作過程中保持數據一致性和正確性。在多線程環境中,線程是并發執行的,因此如果多個線程同時訪問和修改共享資源,可能會導致數據不一致、競態條件(race condition)等問題。線程同步通過協調線程的執行順序和共享資源的訪問來避免這些問題。
在多線程編程中,需要線程同步的主要原因包括:
-
共享資源的安全訪問:多個線程可能同時訪問和修改共享的數據或資源,如果沒有同步機制,可能會導致數據不一致或損壞。
-
避免競態條件:競態條件是指多個線程以不受控制的順序訪問共享資源,從而導致程序執行結果不確定的情況。通過同步機制可以避免競態條件的發生。
-
保證程序正確性:線程同步確保多線程程序的行為是可預測和正確的,避免因并發訪問而引入的錯誤。
1.互斥量(Mutex)
互斥量(互斥鎖)是一種最基本的同步機制,用于保護臨界區(Critical Section),確保在同一時間只有一個線程可以訪問共享資源。
它提供了兩個主要操作:
- 加鎖(Locking):線程通過加鎖操作獲取互斥量,如果互斥量已經被其他線程鎖定,則當前線程會阻塞,直到獲取到鎖。
- 解鎖(Unlocking):線程使用解鎖操作釋放互斥量,允許其他線程獲取鎖。
通過這種機制,互斥量確保了臨界區中的代碼在同一時間只能被一個線程執行,從而避免了數據競爭和不一致的問題。
1.1初始化和銷毀互斥量:
1.1.1pthread_mutex_init()
初始化互斥量,并可選地設置其屬性。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
參數:
pthread_mutex_t *mutex:指向互斥量變量的指針,用于初始化該互斥量。
const pthread_mutexattr_t *attr:可選參數,指向 pthread_mutexattr_t 類型的指針,用于設置互斥量的屬性。如果為 NULL,使用默認屬性。
返回值:
成功:返回 0。
失敗:返回錯誤號(例如 EINVAL 表示參數無效)。
- 初始化互斥量后需要調用
pthread_mutex_destroy
函數來釋放其占用的資源。 - 通常在使用互斥量前調用,確保互斥量的準備和設置。
1.1.2pthread_mutex_destroy()
銷毀互斥量,釋放其占用的資源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
參數:
pthread_mutex_t *mutex:指向要銷毀的互斥量變量的指針。
返回值:
成功:返回 0。
失敗:返回錯誤號。
- 在互斥量不再需要時調用,以釋放其占用的資源。
- 僅當確保沒有線程在使用該互斥量時才能安全地調用該函數。
1.2加鎖和解鎖操作:
1.2.1pthread_mutex_lock()
加鎖操作,阻塞當前線程直到獲取鎖。
int pthread_mutex_lock(pthread_mutex_t *mutex);
參數:
pthread_mutex_t *mutex:指向要加鎖的互斥量變量的指針。
返回值:
成功:返回 0。
失敗:返回錯誤號。
- 當互斥量已經被其他線程鎖定時,當前線程會阻塞直到獲取鎖。
- 必須成對使用
pthread_mutex_lock
和pthread_mutex_unlock
來保護臨界區。
1.2.2pthread_mutex_trylock()
嘗試加鎖操作,非阻塞式,立即返回結果。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
參數:
pthread_mutex_t *mutex:指向要加鎖的互斥量變量的指針。
返回值:
成功:返回 0。
失敗:返回 EBUSY(互斥量已被鎖定)或其他錯誤號。
- 如果互斥量已被鎖定,則立即返回錯誤。
- 適用于需要非阻塞嘗試加鎖的情況,可以用于避免線程阻塞。
1.2.3pthread_mutex_unlock()
解鎖操作,釋放互斥量。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
參數:
pthread_mutex_t *mutex:指向要解鎖的互斥量變量的指針。
返回值:
成功:返回 0。
失敗:返回錯誤號。
- 解鎖后允許其他線程獲取互斥量。
- 必須在每次成功調用
pthread_mutex_lock
后調用pthread_mutex_unlock
來釋放鎖。
示例代碼:
創建兩個線程來共享一個全局變量 int number
,然后每個線程分別對其進行5000次遞增操作,同時使用互斥量來保證線程同步。(如果沒有鎖大家可以看看會發生什么情況)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>#define NUM_THREADS 2
#define NUM_INCREMENTS 5000int number = 0;
pthread_mutex_t mutex; // 互斥量變量void* thread_function(void* arg) {int thread_id = *(int*)arg;for (int i = 0; i < NUM_INCREMENTS; ++i) {// 加鎖pthread_mutex_lock(&mutex);// 臨界區:對共享變量 number 執行操作number++;// 解鎖pthread_mutex_unlock(&mutex);}pthread_exit(NULL);
}int main() {pthread_t threads[NUM_THREADS];int thread_ids[NUM_THREADS];// 初始化互斥量if (pthread_mutex_init(&mutex, NULL) != 0) {fprintf(stderr, "Mutex initialization failed\n");return 1;}// 創建兩個線程for (int i = 0; i < NUM_THREADS; ++i) {thread_ids[i] = i;if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {fprintf(stderr, "Thread creation failed\n");return 1;}}// 等待所有線程結束for (int i = 0; i < NUM_THREADS; ++i) {pthread_join(threads[i], NULL);}// 銷毀互斥量pthread_mutex_destroy(&mutex);// 輸出最終的 number 值printf("Final value of number: %d\n", number);return 0;
}
1.3死鎖(Deadlock)
死鎖并不是linux提供給用戶的一種使用方法,而是由于用戶使用互斥鎖不當引起的一種現象。死鎖發生在多個并發執行的線程(或進程)之間,主要由于彼此競爭資源而造成。典型的死鎖情況涉及兩個或多個線程或進程,每個都在等待對方釋放其持有的資源,導致所有線程都被阻塞,無法繼續執行下去。
常見的死鎖有兩種:
第一種:自己鎖自己,如下圖代碼片段
第二種 線程A擁有A鎖,請求獲得B鎖;線程B擁有B鎖,請求獲得A鎖,這樣造成線程A和線程B都不釋放自己的鎖,而且還想得到對方的鎖,從而產生死鎖,如下圖所示:
- 如何解決死鎖:
- 讓線程按照一定的順序去訪問共享資源
- 在訪問其他鎖的時候,需要先將自己的鎖解開
- 調用pthread_mutex_trylock,如果加鎖不成功會立刻返回
2. 條件變量(Condition Variable)
條件變量(Condition Variable) 是一種線程間同步的機制,通常與互斥鎖結合使用,用于在某個條件滿足時通知其他線程。條件變量允許線程在等待某個特定條件的同時阻塞,直到另一個線程顯式地通知條件已經滿足或者超時。條件本身不是鎖!但它也可以造成線程阻塞。通常與互斥鎖配合使用。給多線程提供一個會合的場所。
- 使用互斥量保護共享數據。
- 使用條件變量可以使線程阻塞, 等待某個條件的發生, 當條件滿足的時候解除阻塞。
主要作用包括:
- 等待條件:使線程能夠在滿足特定條件之前進入休眠狀態,節省系統資源。
- 條件滿足時通知:一旦其他線程改變了條件,可以通過條件變量通知正在等待的線程繼續執行。
2.1相關操作函數
2.1.1pthread_cond_init()
初始化條件變量 cond
。
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
參數:
pthread_cond_t *restrict cond:指向要初始化的條件變量的指針。
const pthread_condattr_t *restrict attr:可選參數,指向 pthread_condattr_t 類型的指針,用于設置條件變量的屬性。通常可以設置為 NULL,表示使用默認屬性。
返回值:
成功:返回 0。
失敗:返回錯誤號(例如 EINVAL 表示參數無效)。
- 初始化條件變量后,應當使用
pthread_cond_destroy
函數來釋放其占用的資源。 - 通常在創建條件變量后立即調用該函數進行初始化。
2.1.2pthread_cond_destroy()
銷毀條件變量 cond
,釋放其占用的資源。
int pthread_cond_destroy(pthread_cond_t *cond);
參數:
pthread_cond_t *cond:指向要銷毀的條件變量的指針。
返回值:
成功:返回 0。
失敗:返回錯誤號。
- 在條件變量不再需要時調用,以釋放其占用的資源。
- 確保沒有線程在使用該條件變量時才能安全地調用該函數。
2.1.3pthread_cond_wait()
阻塞當前線程,等待條件變量 cond
被其他線程信號喚醒。
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
參數:
pthread_cond_t *restrict cond:指向要等待的條件變量的指針。
pthread_mutex_t *restrict mutex:與條件變量相關聯的互斥鎖,用于避免競態條件。
返回值:
成功:返回 0。
失敗:返回錯誤號。
- 調用該函數前必須先獲取
mutex
鎖,函數內部會自動釋放mutex
鎖,并在等待期間阻塞當前線程。 - 當被喚醒時,函數內部會再次獲取
mutex
鎖,并從函數返回。
2.1.4pthread_cond_timedwait()
在指定的超時時間內阻塞當前線程,等待條件變量 cond
被其他線程信號喚醒。
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
參數:
pthread_cond_t *restrict cond:指向要等待的條件變量的指針。
pthread_mutex_t *restrict mutex:與條件變量相關聯的互斥鎖。
const struct timespec *restrict abstime:指定的超時時間,為絕對時間。
返回值:
成功:返回 0。
失敗:返回錯誤號。
- 與
pthread_cond_wait
類似,但可以設置超時時間,在超時之后會自動返回并解除阻塞。 - 如果超時時間設置為
NULL
,則函數將無限期地等待,直到條件變量被信號喚醒。
2.1.5pthread_cond_signal()
喚醒等待在條件變量 cond
上的一個線程。
int pthread_cond_signal(pthread_cond_t *cond);
參數:
pthread_cond_t *cond:指向要喚醒的條件變量的指針。
返回值:
成功:返回 0。
失敗:返回錯誤號。
- 喚醒等待在條件變量上的一個線程,如果有多個線程等待,則可能喚醒任意一個。
- 喚醒后,被喚醒的線程將嘗試重新獲取與條件變量關聯的互斥鎖。
示例代碼:
3. 讀寫鎖(Read-Write Lock)
讀寫鎖允許多個線程同時對共享資源進行讀取操作,但是寫操作時需要排他性,即同一時刻只能有一個線程進行寫操作。這種區分讀和寫的方式能夠有效地提高系統的并發性能,特別適用于數據結構中讀操作遠遠多于寫操作的情況。當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當它以寫模式鎖住時,它是以獨占模式鎖住的。寫獨占、讀共享。
讀寫鎖特性:
- 讀寫鎖是“寫模式加鎖”時,解鎖前,所有對該鎖加鎖的線程都會被阻塞。
- 讀寫鎖是“讀模式加鎖”時,如果線程以讀模式對其加鎖會成功;如果線程以寫模式加鎖會阻塞。
- 讀寫鎖是“讀模式加鎖”時, 既有試圖以寫模式加鎖的線程,也有試圖以讀模式加鎖的線程。那么讀寫鎖會阻塞隨后的讀模式鎖請求。優先滿足寫模式鎖。讀鎖、寫鎖并行阻塞,寫鎖優先級高
3.1初始化和銷毀讀寫鎖
3.1.1pthread_rwlock_init()
初始化讀寫鎖 rwlock
,可以選擇性地設置其屬性。
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
參數:
pthread_rwlock_t *restrict rwlock:指向要初始化的讀寫鎖變量的指針。
const pthread_rwlockattr_t *restrict attr:可選參數,指向 pthread_rwlockattr_t 類型的指針,用于設置讀寫鎖的屬性。通常可以設置為 NULL,表示使用默認屬性。
返回值:
成功:返回 0。
失敗:返回錯誤號(例如 EINVAL 表示參數無效)。
- 初始化讀寫鎖后,應當使用
pthread_rwlock_destroy
函數來釋放其占用的資源。 - 通常在創建讀寫鎖后立即調用該函數進行初始化。
3.1.2pthread_rwlock_destroy()
銷毀讀寫鎖 rwlock
,釋放其占用的資源。
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
參數:
pthread_rwlock_t *rwlock:指向要銷毀的讀寫鎖變量的指針。
返回值:
成功:返回 0。
失敗:返回錯誤號。
- 在讀寫鎖不再需要時調用,以釋放其占用的資源。
- 確保沒有線程在使用該讀寫鎖時才能安全地調用該函數。
3.2讀寫鎖加鎖和解鎖操作:
3.2.1pthread_rwlock_rdlock()
加讀鎖,允許多個線程同時對共享資源進行讀取操作。
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
參數:
pthread_rwlock_t *rwlock:指向要加讀鎖的讀寫鎖變量的指針。
返回值:
成功:返回 0。
失敗:返回錯誤號。
- 如果有其他線程持有寫鎖或者有線程在請求寫鎖,則當前線程將被阻塞,直到獲取讀鎖為止。
- 多個線程可以同時獲取讀鎖,不會相互阻塞。
3.2.2pthread_rwlock_wrlock()
加寫鎖,確保只有一個線程可以對共享資源進行寫操作,期間禁止其他線程的讀或寫操作。
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
參數:
pthread_rwlock_t *rwlock:指向要加寫鎖的讀寫鎖變量的指針。
返回值:
成功:返回 0。
失敗:返回錯誤號。
- 如果有其他線程持有讀鎖或寫鎖,則當前線程將被阻塞,直到獲取寫鎖為止。
- 只能有一個線程可以同時持有寫鎖,其他線程無法同時獲取讀鎖或寫鎖。
3.2.3pthread_rwlock_tryrdlock()
嘗試加讀鎖,非阻塞方式。如果不能立即獲得鎖,則立即返回。
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
參數:
pthread_rwlock_t *rwlock:指向要加讀鎖的讀寫鎖變量的指針。
返回值:
成功:返回 0。
失敗:返回 EBUSY(讀寫鎖已被寫鎖占用)或其他錯誤號。
- 如果不能立即獲取讀鎖,則該函數立即返回失敗。
- 適用于需要檢測是否可以立即讀取共享資源的場景。
3.2.4pthread_rwlock_trywrlock()
嘗試加寫鎖,非阻塞方式。如果不能立即獲得鎖,則立即返回。
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
參數:
pthread_rwlock_t *rwlock:指向要加寫鎖的讀寫鎖變量的指針。
返回值:
成功:返回 0。
失敗:返回 EBUSY(讀寫鎖已被其他線程占用)或其他錯誤號。
- 如果不能立即獲取寫鎖,則該函數立即返回失敗。
- 適用于需要檢測是否可以立即寫入共享資源的場景。
3.2.5pthread_rwlock_unlock()
解鎖操作,釋放之前加的讀鎖或寫鎖。
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
參數:
pthread_rwlock_t *rwlock:指向要解鎖的讀寫鎖變量的指針。
返回值:
成功:返回 0。
失敗:返回錯誤號。
- 解鎖后允許其他線程獲取讀鎖或寫鎖。
- 必須在每次成功調用
pthread_rwlock_rdlock
或pthread_rwlock_wrlock
后調用該函數來釋放鎖。
示例代碼:
其中包括3個寫線程和5個讀線程對同一個全局資源進行操作。每個線程都會不定時地訪問和修改這個全局資源,并通過讀寫鎖確保線程之間的同步。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h> // 用于隨機睡眠時間#define NUM_READERS 5
#define NUM_WRITERS 3pthread_rwlock_t rwlock;
int global_resource = 0;void* writer(void* arg) {int thread_id = *(int*)arg;while (1) {// 模擬寫操作sleep(rand() % 3); // 隨機睡眠時間// 加寫鎖pthread_rwlock_wrlock(&rwlock);// 寫操作global_resource++;printf("Writer %d writes: global_resource = %d\n", thread_id, global_resource);// 解鎖pthread_rwlock_unlock(&rwlock);}return NULL;
}void* reader(void* arg) {int thread_id = *(int*)arg;while (1) {// 模擬讀操作sleep(rand() % 2); // 隨機睡眠時間// 加讀鎖pthread_rwlock_rdlock(&rwlock);// 讀操作printf("Reader %d reads: global_resource = %d\n", thread_id, global_resource);// 解鎖pthread_rwlock_unlock(&rwlock);}return NULL;
}int main() {pthread_t writers[NUM_WRITERS], readers[NUM_READERS];int writer_ids[NUM_WRITERS], reader_ids[NUM_READERS];int i;// 初始化讀寫鎖pthread_rwlock_init(&rwlock, NULL);// 創建寫線程for (i = 0; i < NUM_WRITERS; ++i) {writer_ids[i] = i + 1;pthread_create(&writers[i], NULL, writer, &writer_ids[i]);}// 創建讀線程for (i = 0; i < NUM_READERS; ++i) {reader_ids[i] = i + 1;pthread_create(&readers[i], NULL, reader, &reader_ids[i]);}// 等待所有寫線程結束for (i = 0; i < NUM_WRITERS; ++i) {pthread_join(writers[i], NULL);}// 等待所有讀線程結束for (i = 0; i < NUM_READERS; ++i) {pthread_join(readers[i], NULL);}// 銷毀讀寫鎖pthread_rwlock_destroy(&rwlock);return 0;
}
4. 信號量(Semaphore)
信號量是由一個整型變量和相關的操作集合組成,用于控制對共享資源的訪問。信號量可以看作是一個計數器,用于表示可用的資源數量,線程或進程在訪問資源前必須首先獲取信號量,訪問結束后釋放信號量。
主要作用包括:
- 同步:控制多個線程或進程的執行順序,保證在某些條件下的有序執行。
- 互斥:保證對共享資源的訪問是排他的,避免多個線程或進程同時修改資源造成的數據不一致性問題。
4.2相關操作函數
4.2.1sem_init()
初始化一個信號量 sem
。
int sem_init(sem_t *sem, int pshared, unsigned int value);
參數:
sem_t *sem:指向要初始化的信號量的指針。
int pshared:指定信號量是進程共享(非零)還是線程共享(零)。
unsigned int value:信號量的初始值,表示可用資源的數量。
返回值:
成功:返回 0。
失敗:返回 -1,并設置 errno 來指示錯誤原因。
- 信號量初始化后需要通過
sem_destroy
函數進行清理。 - 如果
pshared
是非零,則信號量可以在多個進程間共享,通常使用在進程間通信(IPC)的場景中。
4.2.2sem_destroy()
銷毀一個已經初始化的信號量 sem
。釋放由信號量占用的資源,確保在信號量不再需要時調用。
int sem_destroy(sem_t *sem);
參數:
sem_t *sem:指向要銷毀的信號量的指針。
返回值:
成功:返回 0。
失敗:返回 -1,并設置 errno 來指示錯誤原因。
4.2.3.sem_wait()
對信號量 sem
進行 P 操作(等待操作)。
int sem_wait(sem_t *sem);
參數:
sem_t *sem:指向要操作的信號量的指針。
返回值:
成功:返回 0。
失敗:返回 -1,并設置 errno 來指示錯誤原因。
- 如果信號量的值大于
0
,將其遞減;否則阻塞當前線程,直到信號量的值大于0
。 - 該函數執行時需要保證線程安全,通常與互斥鎖結合使用以避免競態條件。
4.2.4sem_trywait()
嘗試對信號量 sem
進行 P 操作的非阻塞版本。與 sem_wait
不同的是,如果信號量的值為 0
,立即返回而不阻塞線程。
int sem_trywait(sem_t *sem);
參數:
sem_t *sem:指向要操作的信號量的指針。
返回值:
成功:返回 0。
如果信號量的值為 0,表示資源不可用,立即返回 -1(不阻塞),并設置 errno 為 EAGAIN。
其他失敗情況返回 -1,并設置 errno 來指示錯誤原因。
4.2.5sem_post()
對信號量 sem
進行 V 操作(釋放操作)。
int sem_post(sem_t *sem);
參數:
sem_t *sem:指向要操作的信號量的指針。
返回值:
成功:返回 0。
失敗:返回 -1,并設置 errno 來指示錯誤原因。
- 將信號量的值遞增,如果有線程因等待該信號量而被阻塞,將會喚醒其中一個線程。
- 釋放操作通常在資源使用完畢后調用,通知其他線程可以繼續訪問該資源。
4.2.6sem_getvalue()
獲取信號量 sem
的當前值。sem_getvalue
函數可以獲取信號量的當前值,而無需對其進行修改。
int sem_getvalue(sem_t *restrict sem, int *restrict sval);
參數:
sem_t *restrict sem:指向要獲取值的信號量的指針。
int *restrict sval:用于存儲信號量當前值的整型指針。
返回值:
成功:返回 0,并將當前信號量的值存儲在 sval 中。
失敗:返回 -1,并設置 errno 來指示錯誤原因。