【Linux應用編程】POSIX線程互斥與同步機制—讀寫鎖


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,讀寫鎖實例地址,不能爲NULL
  • abstime,超時時間,單位爲時鐘節拍
  • 返回
返回值 描述
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,讀寫鎖實例地址,不能爲NULL
  • abstime,超時時間,單位爲時鐘節拍
  • 返回
返回值 描述
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,讀寫鎖屬性實例地址,不能爲NULL
  • pshared,作用域類型,PTHREAD_PROCESS_PRIVATEPTHREAD_PROCESS_SHARED
  • 成功返回0,參數無效返回 EINVAL

5 總結

  讀寫鎖是一種適用於“多讀少寫”場景模型的線程間互斥機制。讀寫鎖本質還是鎖,因此仍需關注“死鎖”問題,謹慎使用。同時由於讀線程比寫線程多,注意讀寫鎖一直被讀線程搶佔持有而導致寫線程“飢餓”現象。讀寫鎖的使用注意事項,結合互斥鎖文章2.3節的"互斥鎖使用原則",參考2.3節的“讀寫鎖使用原則”。


6 參考文章

【1】多線程併發之讀寫鎖(ReentranReadWriteLock&ReadWriteLock)使用詳解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章