文章目錄
1 前言
在上一篇文章中,主要描述了條件變量的含義、屬性、使用原則以及使用方法。條件變量往往是和互斥鎖一起使用的,所以把條件變量機制先寫了。本文描述讀寫鎖,讀寫鎖與互斥鎖類似,但又有不同點。在實際應用中,存在這樣“多讀少寫”或者“多讀多寫”的應用模型,如文件支持多個線程讀和多個線程寫。對於這種場景,雖然可以使用互斥鎖,但效率明顯降低,因此引入“讀寫鎖”機制,讀寫鎖支持多個線程“併發”讀。
2 讀寫鎖
讀寫鎖( Read/Write lock )是線程間互斥的一種機制。讀寫鎖本質是一把鎖,與互斥鎖類似,“讀 ~ 寫”和“寫 ~ 寫”過程是互斥的,但讀寫鎖把上鎖過程劃分爲讀鎖過程和寫鎖過程,使得“讀 ~ 讀”過程是共享的,而不是互斥鎖那種“平衡”鎖。讀寫鎖最大的特點是“多讀單寫”,即是同一時刻可以支持多個線程持有讀鎖,只有一個線程可以持有寫鎖。這樣可以支持線程併發讀數據,大大提高讀的效率。 理論上,讀寫鎖比互斥鎖允許對於共享數據更大程度的併發。與互斥鎖相比,讀寫鎖是否能夠提高性能取決於讀寫數據的頻率、讀取和寫入操作的持續時間、以及讀線程和寫線程之間的競爭。
2.1 讀寫鎖特點
- 用於線程互斥
- 讀鎖支持併發,寫鎖只支持互斥
- 讀讀共享,讀寫互斥,寫寫互斥
- 可引起線程睡眠
2.2 讀寫鎖適用場景
- 共享數據量較大
- 讀比寫更爲頻繁
- 支持非阻塞場景
2.3 讀寫鎖使用原則
讀寫鎖與互斥鎖有共同點,讀寫鎖使用原則可以參考互斥鎖的使用原則。
- 持有鎖後必須釋放,防止鎖異常異常,雖然讀鎖可以共享;寫鎖不釋放,導致死鎖
- 線程多次持有鎖後, 必須依次釋放所有鎖
- “讀多寫少”,由於讀線程比寫線程多,讀線程應適當主動釋放cpu,防止寫線程長時間申請不到鎖處於“飢餓”狀態;或者讓寫線程優先級高於讀線程
3 讀寫鎖使用
讀寫鎖使用的基本步驟爲:
【1】創建讀寫鎖實例
【2】初始化讀寫鎖
【3】持有讀寫鎖鎖
【4】釋放讀寫鎖
【5】銷燬讀寫鎖實例
3.1 創建讀寫鎖
posix線程讀寫鎖以pthread_rwlock_t
數據結構表示。讀寫鎖實例可以用靜態和動態創建。
pthread_rwlock_t rwlock;
3.2 初始化讀寫鎖
讀寫鎖初始化可以使用pthread_rwlock_init
動態初始化,也可以使用宏 PTHREAD_RWLOCK_INITIALIZER
實現靜態初始化, PTHREAD_RWLOCK_INITIALIZER
是POSIX定義的一個結構體常量 。
動態初始化
int pthread_rwlock_init(pthread_rwlock_t *rwlock, pthread_rwlockattr_t *attr);
-
rwlock
,讀寫鎖實例地址,不能爲NULL -
attr
,讀寫鎖屬性地址,傳入NULL表示使用默認屬性;大部分場合使用默認屬性即可,關於屬性詳見第四節。 -
返回,成功返回0,參數無效返回 EINVAL
靜態初始化
使用宏PTHREAD_RWLOCK_INITIALIZER
的靜態初始化方式等價於使用pthread_rwlock_init
採用默認屬性(attr
傳入NULL)的動態初始化,不同之處在於PTHREAD_RWLOCK_INITIALIZER
宏沒有相關錯誤參數的檢查。
使用例子:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
3.3 讀寫鎖讀鎖上鎖
讀寫鎖讀鎖申請持有分爲阻塞方式和非阻塞方式,常用的一般是阻塞方式。
3.3.1 阻塞方式
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
-
rwlock
,讀寫鎖實例地址,不能爲NULL -
返回
返回值 描述 0 成功 EINVAL 參數無效 EDEADLK 死鎖
如果讀寫鎖的寫鎖還沒有被其他線程持有(上鎖),則申請持有讀鎖的線程獲得鎖。如果讀寫鎖的寫鎖被其他線程持有,則當前線程將被阻塞,直到持讀寫鎖的寫鎖的線程解鎖後才喚醒繼續執行讀操作。所有等待讀鎖的線程按照先進先出的原則獲取鎖。
3.3.2 指定超時時間阻塞方式
int pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock, const struct timespec *abstime);
rwlock
,讀寫鎖實例地址,不能爲NULLabstime
,超時時間,單位爲時鐘節拍- 返回
返回值 | 描述 |
---|---|
0 | 成功 |
EINVAL | 參數無效 |
EDEADLK | 死鎖 |
ETIMEDOUT | 超時 |
該函數不會無限等待,超出指定時間節拍後,仍未申請到讀寫鎖的讀鎖會返回,返回ETIMEDOUT
。
3.3.3 非阻塞方式
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
rwlock
,讀寫鎖實例地址,不能爲NULL- 返回
返回值 | 描述 |
---|---|
0 | 成功 |
EINVAL | 參數無效 |
EDEADLK | 死鎖 |
EBUSY | 鎖被其他線程持有 |
調用該函數會立即返回,不會引起線程睡眠。實際應用可以根據返回狀態執行不同的任務操作。
3.4 讀寫鎖寫鎖上鎖
讀寫鎖寫鎖申請持有分爲阻塞方式和非阻塞方式,常用的一般是阻塞方式。寫鎖與互斥鎖機制一樣,互斥訪問,在沒有其他線程持有讀寫鎖(包括讀鎖和寫鎖)時,線程才能成功申請持有寫鎖。
3.4.1 阻塞方式
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
-
rwlock
,讀寫鎖實例地址,不能爲NULL -
返回
返回值 描述 0 成功 EINVAL 參數無效 EDEADLK 死鎖
如果讀寫鎖的讀鎖和寫鎖還沒有被其他線程持有(上鎖),則申請持有寫鎖的線程獲得鎖。如果讀寫鎖的讀鎖或者寫鎖被其他線程持有,則當前線程將被阻塞,直到持讀寫鎖的讀鎖和寫鎖的線程解鎖後才喚醒繼續執行寫操作。所有等待寫鎖的線程按照先進先出的原則獲取鎖。
3.4.2 指定超時時間阻塞方式
int pthread_rwlock_timedwrlock(pthread_rwlock_t *rwlock, const struct timespec *abstime);
rwlock
,讀寫鎖實例地址,不能爲NULLabstime
,超時時間,單位爲時鐘節拍- 返回
返回值 | 描述 |
---|---|
0 | 成功 |
EINVAL | 參數無效 |
EDEADLK | 死鎖 |
ETIMEDOUT | 超時 |
該函數不會無限等待,超出指定時間節拍後,仍未申請到讀寫鎖的讀鎖會返回,返回ETIMEDOUT
。
3.4.3 非阻塞方式
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
rwlock
,讀寫鎖實例地址,不能爲NULL- 返回
返回值 | 描述 |
---|---|
0 | 成功 |
EINVAL | 參數無效 |
EDEADLK | 死鎖 |
EBUSY | 鎖被其他線程持有 |
調用該函數會立即返回,不會引起線程睡眠。實際應用可以根據返回狀態執行不同的任務操作。
3.5 讀寫鎖釋放
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
rwlock
,讀寫鎖實例地址,不能爲NULL- 返回
返回值 | 描述 |
---|---|
0 | 成功 |
EINVAL | 參數無效 |
EDEADLK | 死鎖 |
EBUSY | 鎖被其他線程持有 |
同一線程多次申請持有鎖,必須依次釋放鎖。
3.6 讀寫鎖銷燬
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
rwlock
,讀寫鎖實例地址,不能爲NULL- 返回
返回值 | 描述 |
---|---|
0 | 成功 |
EINVAL | cond已被銷燬過,或者cond爲空 |
EBUSY | 讀寫鎖被其他線程使用 |
pthread_rwlock_destroy
用於銷燬一個已經使用動態初始化的讀寫鎖。銷燬後的讀寫鎖處於未初始化狀態,讀寫鎖的屬性和控制塊參數處於不可用狀態。使用銷燬函數需要注意幾點:
- 已銷燬的讀寫鎖,可以使用
pthread_rwlock_init
重新初始化使用 - 不能重複銷燬已銷燬的讀寫鎖
- 使用宏
PTHREAD_RWLOCK_INITIALIZER
靜態初始化的讀寫鎖不能銷燬 - 沒有線程持有讀寫鎖時,且該鎖沒有阻塞任何線程,才能銷燬
3.7 寫個例子
代碼實現功能:
- 創建一個“多讀 ~ 少寫”模型
- 創建三個個線程,一個線程負責寫數據,另外兩個線程讀取數據並輸出到終端
- 三個線程共享一個內存,通過讀寫鎖互斥訪問
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <stdlib.h>
#include "pthread.h"
struct _buff_node
{
uint8_t buf[64];
uint32_t occupy_size;
};
/* 靜態方式創建初始化讀寫鎖 */
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
/* 共享緩存 */
static struct _buff_node s_buf;
void *thread0_entry(void *data)
{/* 讀線程0 */
uint8_t i =0;
for (;;)
{
pthread_rwlock_rdlock(&rwlock);
if (s_buf.occupy_size != 0)
{
printf("thread0 read data:");
for (i=0; i<s_buf.occupy_size; i++)
{
printf("0x%02x ", s_buf.buf[i]);
}
printf("\r\n");
}
usleep(10); /* 解讀鎖前,主動讓出cpu,讓讀線程2調度 */
pthread_rwlock_unlock(&rwlock);
sleep(1); /* 主動讓出cpu */
}
}
void *thread1_entry(void *data)
{/* 讀線程1 */
uint8_t i =0;
for (;;)
{
pthread_rwlock_rdlock(&rwlock);
if (s_buf.occupy_size != 0)
{
printf("thread1 read data:");
for (i=0; i<s_buf.occupy_size; i++)
{
printf("0x%02x ", s_buf.buf[i]);
}
printf("\r\n");
}
usleep(10); /* 解讀鎖前,主動讓出cpu,讓讀線程2調度 */
pthread_rwlock_unlock(&rwlock);
sleep(1); /* 主動讓出cpu */
}
}
void *thread2_entry(void *data)
{/* 寫線程1 */
uint8_t i =0;
for (;;)
{
pthread_rwlock_wrlock(&rwlock);
s_buf.occupy_size = 0;
for (i = 0;i<8; i++)
{
s_buf.buf[i] = rand();
s_buf.occupy_size++;
}
printf("thread2 write %dByte data\n", s_buf.occupy_size);
pthread_rwlock_unlock(&rwlock);
sleep(1); /* 1秒週期寫數據 */
}
}
int main(int argc, char **argv)
{
pthread_t thread0,thread1,thread2;
void *retval;
pthread_create(&thread0, NULL, thread0_entry, NULL);
pthread_create(&thread1, NULL, thread1_entry, NULL);
pthread_create(&thread2, NULL, thread2_entry, NULL);
pthread_join(thread0, &retval);
pthread_join(thread1, &retval);
pthread_join(thread2, &retval);
return 0;
}
輸出結果
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/rwlock$ gcc rwlock.c -o rwlock -lpthread
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/rwlock$ ./rwlock
thread2 write 8Byte data
thread2 write 8Byte data
thread1 read data:0x29 0xcd 0xba 0xab 0xf2 0xfb 0xe3 0x46
thread0 read data:0x29 0xcd 0xba 0xab 0xf2 0xfb 0xe3 0x46
thread2 write 8Byte data
thread0 read data:0x7c 0xc2 0x54 0xf8 0x1b 0xe8 0xe7 0x8d
thread1 read data:0x7c 0xc2 0x54 0xf8 0x1b 0xe8 0xe7 0x8d
代碼中,對printf
函數加鎖,實際使用是不允許的,違反了加鎖的原則,這裏只是模擬場景測試。
4 讀寫鎖屬性
使用默認的讀寫鎖屬性可以滿足絕大部分的應用場景,特殊場景也可以調整讀寫鎖屬性。下面描述主要的讀寫鎖屬性及API。讀寫鎖屬性設置,基本步驟爲:
【1】創建讀寫鎖屬性實例
【2】初始化屬性實例
【3】設置屬性
【4】銷燬屬性實例
4.1 創建讀寫鎖屬性
posix線程讀寫鎖屬性以pthread_rwlockattr_t
數據結構表示。讀寫鎖屬性實例可以用靜態和動態創建。
pthread_rwlockattr_t attr;
4.2 讀寫鎖屬性初始化與銷燬
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
attr
,讀寫鎖屬性實例地址,不能爲NULL- 成功返回0,參數無效返回 EINVAL
設置讀寫鎖屬性時,首先創建一個屬性pthread_rwlockattr_t
實例,然後調用pthread_rwlockattr_init
函數初始實例,接下來就是屬性設置。初始化後的屬性值就是默認讀寫鎖屬性,等價於使用pthread_rwlock_init
採用默認屬性(attr
傳入NULL)的初始化。
4.3 讀寫鎖作用域
讀寫鎖作用域表示讀寫鎖的作用範圍,分爲進程內(創建者)作用域PTHREAD_PROCESS_PRIVATE
和跨進程作用域PTHREAD_PROCESS_SHARED
。進程內作用域只能用於進程內線程互斥,跨進程可以用於系統所有線程間互斥。
作用域設置與獲取函數
int pthread_rwlockattr_setshared(pthread_rwlockattr_t *attr,int pshared);
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
attr
,讀寫鎖屬性實例地址,不能爲NULLpshared
,作用域類型,PTHREAD_PROCESS_PRIVATE
和PTHREAD_PROCESS_SHARED
- 成功返回0,參數無效返回 EINVAL
5 總結
讀寫鎖是一種適用於“多讀少寫”場景模型的線程間互斥機制。讀寫鎖本質還是鎖,因此仍需關注“死鎖”問題,謹慎使用。同時由於讀線程比寫線程多,注意讀寫鎖一直被讀線程搶佔持有而導致寫線程“飢餓”現象。讀寫鎖的使用注意事項,結合互斥鎖文章2.3節的"互斥鎖使用原則",參考2.3節的“讀寫鎖使用原則”。