[TOC] 1. 線程同步概述 線程同步定義 線程同步,指的是控制多線程間的相對執行順序,從而線上程間正確、有序地共用數據,以下為線程同步常見使用場合。 多線程執行的任務在順序上存在依賴關係 線程間共用數據只能同時被一個線程使用 線程同步方法 在實際項目中,經常使用的線程同步方法主要分為三種: 互斥 ...
目錄
1. 線程同步概述
線程同步定義
線程同步,指的是控制多線程間的相對執行順序,從而線上程間正確、有序地共用數據,以下為線程同步常見使用場合。
- 多線程執行的任務在順序上存在依賴關係
- 線程間共用數據只能同時被一個線程使用
線程同步方法
在實際項目中,經常使用的線程同步方法主要分為三種:
- 互斥鎖
- 條件變數
- Posix信號量(包括有名信號量和無名信號量)
本節內容只介紹互斥鎖和條件變數,Posix信號量後續在Posix IPC專題中介紹。
2. 互斥鎖
互斥鎖概念
互斥鎖用於確保同一時間只有一個線程訪問共用數據,使用方法為:
- 加鎖
- 訪問共用數據
- 解鎖
對互斥鎖加鎖後,任何其他試圖再次對其加鎖的線程都會被阻塞,直到當前線程釋放該互斥鎖,解鎖時所有阻塞線程都會變成可運行狀態,但究竟哪個先運行,這一點是不確定的。
互斥鎖基本API
初始化與銷毀
互斥鎖是用pthread_mutex_t
數據類型表示的,在使用互斥鎖之前,需要先進行初始化,初始化方法有兩種:
- 設置為常量
PTHREAD_MUTEX_INITIALIZER
,只適用於靜態分配的互斥鎖 - 調用
pthread_mutex_init
函數,靜態分配和動態分配的互斥鎖都可以
互斥鎖使用完以後,可以調用pthread_mutex_destroy
進行銷毀,尤其是對於動態分配的互斥鎖,在釋放記憶體前,調用pthread_mutex_destroy是必須的。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//兩個函數的返回值:成功返回0,失敗返回錯誤編號
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
其中,pthread_mutex_init的第二個參數attr用於設置互斥鎖的屬性,如果要使用預設屬性,只需把attr設為NULL。
上鎖與解鎖
//兩個函數的返回值:成功返回0,失敗返回錯誤編號
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
對互斥鎖上鎖,需要調用pthread_mutex_lock
,如果互斥鎖已經上鎖,調用線程將阻塞到該互斥鎖被釋放。
對互斥鎖解鎖,需要調用pthread_mutex_unlock
。
兩個特殊的上鎖函數
嘗試上鎖
//成功返回0,失敗返回錯誤編號
int pthread_mutex_trylock(pthread_mutex_t *mutex);
如果不希望調用線程阻塞,可以使用pthread_mutex_trylock
嘗試上鎖:
- 若mutex未上鎖,pthread_mutex_trylock將加鎖成功,返回0
- 若mutex已上鎖,pthread_mutex_trylock會加鎖失敗,返回
EBUSY
限時上鎖
//成功返回0,失敗返回錯誤編號
int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *time);
pthread_mutex_timedlock
是一個可以設置阻塞時間的上鎖函數:
- 當mutex已上鎖時,調用線程會阻塞設定的時間
- 當達到設定時間時,pthread_mutex_timedlock將加鎖失敗並解除阻塞,返回
ETIMEDOUT
關於第二個參數time,有兩點需要註意:
- time表示等待的絕對時間,需要將其設為當前時間 + 等待時間
- time是由struct timespec指定的,它由秒和納秒來描述時間
示例代碼
/*
* 測試使用上述4個加鎖函數
*/
#include <pthread.h>
#include <time.h>
#include <errno.h>
#include <stdio.h>
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;
pthread_mutex_t mutex3;
void *thread1_start(void *arg)
{
pthread_mutex_lock(&mutex1);
printf("thread1 has locked mutex1\n");
sleep(2); //保證thread2執行時mutex1還未解鎖
pthread_mutex_unlock(&mutex1);
}
void *thread2_start(void *arg)
{
if (pthread_mutex_trylock(&mutex2) == 0)
printf("thread2 trylock mutex2 sucess\n");
if (pthread_mutex_trylock(&mutex1) == EBUSY)
printf("thread2 trylock mutex1 failed\n");
pthread_mutex_unlock(&mutex2);
}
void *thread3_start(void *arg)
{
struct timespec time;
struct tm *tmp_time;
char s[64];
int err;
pthread_mutex_lock(&mutex3);
printf("thread3 has locked mutex3\n");
/*獲取當前時間,並轉化為本地時間列印*/
clock_gettime(CLOCK_REALTIME, &time);
tmp_time = localtime(&time.tv_sec);
strftime(s, sizeof(s), "%r", tmp_time);
printf("current time is %s\n", s);
/*設置time = 當前時間 + 等待時間10S*/
time.tv_sec = time.tv_sec + 10;
/*mutex3已上鎖,這裡會阻塞*/
if (pthread_mutex_timedlock(&mutex3, &time) == ETIMEDOUT)
printf("pthread_mutex_timedlock mutex3 timeout\n");
/*再次獲取當前時間,並轉化為本地時間列印*/
clock_gettime(CLOCK_REALTIME, &time);
tmp_time = localtime(&time.tv_sec);
strftime(s, sizeof(s), "%r", tmp_time);
printf("the time is now %s\n", s);
pthread_mutex_unlock(&mutex3);
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
/*測試pthread_mutex_lock和pthread_mutex_trylock*/
pthread_mutex_init(&mutex1, NULL);
pthread_mutex_init(&mutex2, NULL);
pthread_create(&tid1, NULL, thread1_start, NULL);
pthread_create(&tid2, NULL, thread2_start, NULL);
if (pthread_join(tid1, NULL) == 0)
{
pthread_mutex_destroy(&mutex1);
}
if (pthread_join(tid2, NULL) == 0)
{
pthread_mutex_destroy(&mutex2);
}
/*測試pthread_mutex_timedlock*/
pthread_mutex_init(&mutex3, NULL);
pthread_create(&tid3, NULL, thread3_start, NULL);
if (pthread_join(tid3, NULL) == 0)
{
pthread_mutex_destroy(&mutex3);
}
return 0;
}
3. 避免死鎖
線程的死鎖概念
線程間死鎖,指的是線程間相互等待臨界資源而造成彼此無法繼續執行的現象。
產生死鎖的四個必要條件
- 互斥條件:資源同時只能被一個線程使用,此時若有其他線程請求該資源,則請求線程必須等待
- 不可剝奪條件:線程獲得的資源在未使用完畢前,不能被其他線程搶奪,只能由獲得該資源的線程主動釋放
- 請求與保持條件:線程已經至少得到了一個資源,但又提出了新的資源請求,而新的資源已被其他線程占有,此時請求線程被阻塞,但對自己已獲得的資源保持不放
- 迴圈等待條件:存在一個資源等待環,環中每一個線程都占有下一個線程所需的至少一個資源
直觀上看,迴圈等待條件似乎和死鎖的定義一樣,其實不然,因為死鎖定義中的要求更為嚴格:
- 迴圈等待條件要求P(i+1)需要的資源,至少有一個來自P(i)即可
- 死鎖定義要求P(i+1)需要的資源,由且僅由P(i)提供
如何避免死鎖
- 所有線程以相同順序加鎖
- 給所有的臨界資源分配一個唯一的序號,對應的線程鎖也分配同樣的序號,系統中的所有線程按照嚴格遞增的次序請求資源
- 使用pthread_mutex_trylock嘗試加鎖,若失敗就放棄上鎖,同時釋放已占有的鎖
- 使用pthread_mutex_timedlock限時加鎖,若超時就放棄上鎖,同時釋放已占有的鎖
4. 條件變數
條件變數概念
- 條件變數是線程另一種可用的同步機制,它給多線程提供了一個回合的場所
- 條件變數本身需要由互斥鎖保護,線程在改變條件之前必須先上鎖,其他線程在獲得互斥鎖之前不會知道條件發生了改變
- 條件變數和互斥鎖一起使用,可以使線程以無競爭的方式等待特定條件的發生
條件變數基本API
初始化與銷毀
條件變數是用pthread_cond_t
數據類型表示的,和互斥鎖類似,條件變數的初始化方法也有兩種:
- 設置為常量
PTHREAD_COND_INITIALIZER
,只適用於靜態分配的條件變數 - 調用
pthread_cond_init
函數,適用於靜態分配和動態分配的條件變數
條件變數使用完以後,可以調用pthread_cond_destroy
進行銷毀,同樣的,如果是動態分配的條件變數,在釋放記憶體前,該操作也是必須的。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//兩個函數的返回值:成功返回0,失敗返回錯誤編號
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);
其中,pthread_cond_init的第二個參數attr用於設置條件變數的屬性,如果要使用預設屬性,只需把attr設為NULL。
等待條件滿足
//兩個函數的返回值:成功返回0,失敗返回錯誤編號
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *timeout);
可以調用pthread_cond_wait
函數等待條件滿足,使用步驟如下,傳遞給函數的互斥鎖對條件進行保護,在條件滿足之前,調用線程將一直阻塞。
- 調用線程將鎖住的互斥量傳給pthread_cond_wait
- pthread_cond_wait自動把調用線程放到等待條件的線程列表上,然後對互斥鎖解鎖
- 當條件滿足,pthread_cond_wait返回時,互斥鎖再次被鎖住
- pthread_cond_wait返回後,調用線程再對互斥鎖解鎖
pthread_cond_timedwait
是一個限時等待條件滿足的函數,如果發生超時時條件還沒滿足,pthread_cond_timedwait將重新對互斥鎖上鎖,然後返回ETIMEDOUT
錯誤。
註意:當條件滿足從pthread_cond_wait和pthread_cond_timedwait返回時,調用線程必須重新計算條件,因為另一個線程可能已經在運行並改變了條件。
給線程發信號
有兩個函數可以用於通知線程條件已經滿足:
pthread_cond_signal
至少能喚醒一個等待該條件的線程pthread_cond_broadcast
可以喚醒等待該條件的所有線程
//兩個函數的返回值:成功返回0,失敗返回錯誤編號
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
在調用上面兩個函數時,我們說這是在給線程發信號,註意,一定要先獲取互斥鎖,再改變條件,然後給線程發信號,最後再對互斥鎖解鎖。
示例代碼
/*
* 結合使用條件變數和互斥鎖進行線程同步
*/
#include <pthread.h>
#include <stdio.h>
static pthread_cond_t cond;
static pthread_mutex_t mutex;
static int cond_value;
static int quit;
void *thread_signal(void *arg)
{
while (!quit)
{
pthread_mutex_lock(&mutex);
cond_value++; //改變條件,使條件滿足
pthread_cond_signal(&cond); //給線程發信號
printf("signal send, cond_value: %d\n", cond_value);
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
void *thread_wait(void *arg)
{
while (!quit)
{
pthread_mutex_lock(&mutex);
/*通過while (cond is true)來保證從pthread_cond_wait成功返回時,調用線程會重新檢查條件*/
while (cond_value == 0)
pthread_cond_wait(&cond, &mutex);
cond_value--;
printf("signal recv, cond_value: %d\n", cond_value);
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_create(&tid1, NULL, thread_signal, NULL);
pthread_create(&tid2, NULL, thread_wait, NULL);
sleep(5);
quit = 1;
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}