C語言 鎖的使用總結
1. C 互斥鎖 mutex
初始化與去初始化
#include <pthread.h>
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屬性初始化一個互斥鎖mutex 。如果 atrr 設為 NULL 或者使用一個預設的 pthread_mutexattr_t 型別都是使用預設屬性進行初始化。
重複初始化一個已經初始化過的鎖會導致未知行為。
pthread_mutex_destroy 可以銷燬一個初始化過的鎖。使用此函式銷燬一個mutex,可以再次初始化。
如果嘗試銷燬一個鎖定狀態的mutex會導致未知行為。
除了使用 pthread_mutex_init 函式對 mutex 進行初始化,還可以使用特定的巨集在宣告 mutex 的時候直接賦值進行靜態初始化。例如:
// 普通mutex
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
// 可遞迴mutex
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
// 有錯誤檢查的mutex,同一執行緒重複加鎖報錯
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
上面那個帶不帶NP字尾取決於系統,我用的Ubuntu18.04對應的巨集為PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP。
加鎖與解鎖
// 普通加鎖,重複加鎖會阻塞程序
int pthread_mutex_lock (pthread_mutex_t *__mutex);
// 重複加鎖不阻塞程序
int pthread_mutex_trylock (pthread_mutex_t *__mutex);
// 帶有超時功能加鎖
int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abs_timeout);
// 解鎖
int pthread_mutex_unlock (pthread_mutex_t *__mutex);
pthread_mutex_lock對一個 mutex 加鎖。如果一個執行緒試圖鎖定一個已經被另一個執行緒鎖定的互斥鎖,那麼該執行緒將被掛起,直到擁有該互斥鎖的執行緒先解鎖該互斥鎖。
預設的 mutex 在同一個執行緒裡再次被加鎖會導致未定義行為,如果定義 mutex 為 PTHREAD_MUTEX_RECURSIVE 型別,即可遞迴 mutex ,則這個鎖可以在同一個執行緒內重複加鎖,每次加鎖計數器+1,每次解鎖計數器-1,當計數器為0 的時候其他執行緒才可以獲取這個鎖。
pthread_mutex_trylock 功能與pthread_mutex_lock,只是當mutex已經是鎖定的時候,pthread_mutex_trylock直接返回錯誤碼EBUSY,而不是阻塞程序。
pthread_mutex_timedlock也是加鎖,但是隻阻塞指定的時間,時間一到還沒能獲取鎖則返回錯誤碼ETIMEDOUT。
pthread_mutex_unlock為解鎖。如果互斥鎖未被鎖定,嘗試解鎖會導致未定義行為。
示例
讓一個數從0加到10,然後再減到0。
#include <pthread.h>
#include <stdio.h>
int gValue=0;
pthread_mutex_t gMutex = PTHREAD_MUTEX_INITIALIZER;
void *add(void*){
pthread_mutex_lock(&gMutex); // 加鎖
for (int i = 0; i < 10; ++i) {
printf("[1]%d ", ++gValue);
}
pthread_mutex_unlock(&gMutex); // 解鎖
}
void *sub(void*){
pthread_mutex_lock(&gMutex); // 加鎖
for (int i = 0; i < 10; ++i) {
printf("[2]%d ", --gValue);
}
pthread_mutex_unlock(&gMutex); // 解鎖
}
int main() {
pthread_t p1, p2;
pthread_create(&p1, NULL, add, NULL);
pthread_create(&p2, NULL, sub, NULL);
pthread_join(p1, NULL);
pthread_join(p2, NULL);
return 0;
}
輸出:
[1]1 [1]2 [1]3 [1]4 [1]5 [1]6 [1]7 [1]8 [1]9 [1]10 [2]9 [2]8 [2]7 [2]6 [2]5 [2]4 [2]3 [2]2 [2]1 [2]0
不加鎖的話輸出就比較亂了。
2. C 讀寫鎖 rwlock
前面說過互斥鎖要麼是lock狀態,要麼是unlock狀態,而且一次只能一個執行緒對其加鎖。也就是說這個鎖是排他性的,每次只能一個執行緒擁有。
讀寫鎖,顧名思義用在讀寫的地方,讀寫的地方要求就是如果是寫的話只能一個執行緒擁有,防止寫錯覆蓋新的值。如果是讀狀態可以多個執行緒擁有,這樣就提高了效率,讀寫鎖用於對資料結構讀的次數遠大於寫的情況。
讀寫鎖可以設定為兩種加鎖狀態,即讀鎖定和寫鎖定狀態。
當處於寫鎖定狀態時,所有加鎖操作都會被阻塞。
當處於讀鎖定狀態時,所有試圖設定讀鎖定都會成功,所有試圖設定寫鎖定都會被阻塞,並且還會阻塞後續所有的讀鎖定加鎖操作,直到所有的讀鎖定都被解鎖。
初始化與去初始化
與互斥鎖使用方式類似,都需要初始化和去初始化操作。
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER;
初始化的時候同樣可以使用常量PTHREAD_RWLOCK_INITIALIZER來定義個預設的讀寫鎖。
加鎖與解鎖
// 加 讀 狀態的鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 不阻塞版本,成功則返回0
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 加 寫 狀態的鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 不阻塞版本,成功則返回0
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 解鎖
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
pthread_rwlock_rdlock 是讀模式下鎖,pthread_rwlock_wrlock 是寫模式下鎖定,這兩種鎖定模式都使用同一個函式pthread_rwlock_unlock進行解鎖。
示例
寫了個非常傻瓜式的小程式來驗證這個讀寫鎖的功能。有兩個函式一個是往數組裡面寫字元,一個是讀字元,裡面都加了sleep模擬耗時的操作。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
char str[10];
size_t pos = 0;
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 每次寫一個字元
void *writeData(void *name)
{
pthread_rwlock_wrlock(&rwlock); // 寫 加鎖
sleep(1);
str[pos] = 'a' + pos;
pos++;
printf("%s %ld write\n", (char *)name, time(NULL));
pthread_rwlock_unlock(&rwlock); // 通用解鎖函式
}
// 讀陣列中字串
void *readData(void *name)
{
pthread_rwlock_rdlock(&rwlock); // 讀 加鎖
sleep(1);
printf("%s %ld read: str = %s\n", (char *)name, time(NULL), str);
pthread_rwlock_unlock(&rwlock); // 通用解鎖函式
}
int main()
{
// 搞了6個執行緒幹起來
pthread_t p[6];
pthread_create(&p[0], NULL, writeData, (void *)"p1"); // 讀
pthread_create(&p[1], NULL, readData, (void *)"p2"); // 寫
pthread_create(&p[2], NULL, writeData, (void *)"p3"); // 讀
pthread_create(&p[3], NULL, readData, (void *)"p4"); // 寫
pthread_create(&p[4], NULL, writeData, (void *)"p5"); // 讀
pthread_create(&p[5], NULL, readData, (void *)"p6"); // 寫
for (int i = 0; i < 6; ++i)
{
pthread_join(p[i], NULL);
}
return 0;
}
如果沒有鎖的話,這幾個操作應該都是隨機的。如果讀和寫函式是用的互斥鎖,那麼這幾個函式的輸出也應該是隨機的。
但是輸出結果是這樣的。
p1 1594130585 write
p4 1594130586 read: str = a
p6 1594130586 read: str = a
p2 1594130586 read: str = a
p3 1594130587 write
p5 1594130588 write
每次輸出read的幾個執行緒都是幾乎同時輸出的,因為當有人鎖定write鎖的時候,沒人可以獲取鎖。當有人鎖定read鎖的時候,其他write的會阻塞,但是其他read不會被阻塞,所以read可以同時執行。
參考部落格:https://blog.csdn.net/shaosunrise/article/details/107620885