LV05-03-线程同步-04-读写锁
本文主要是线程同步——读写锁的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
开发板 | 正点原子 i.MX6ULL Linux阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
STM32开发板 | 正点原子战舰V3(STM32F103ZET6) |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
点击查看相关文件下载
--- | --- |
一、读写锁的概念
1. 基本概念与特性
互斥锁和自旋锁都是只有两个状态,即加锁和不加锁,而且一次只有一个线程可以对其加锁。
还有一种锁叫读写锁,读写锁从字面意思可以知道,它由读锁和写锁两部分构成,如果只读取共享资源用读锁加锁,如果要修改共享资源则用写锁加锁。读写锁也叫做共享互斥锁。当读写锁是读模式锁住时,就可以说成是共享模式锁住。当它是写模式锁住时,就可以说成是互斥模式锁住。
【说明】其实为了更好的区分,我个人更习惯把读写锁看成两把锁,一把读锁,一把写锁,后边的笔记也都这样写了,获取读锁就是读锁加锁,释放读锁就是读锁解锁;获取写锁就是写锁加锁,释放写锁就是写锁解锁。
读写锁有3
种状态:读模式下的加锁状态、写模式下的加锁状态和不加锁状态。
在应用程序当中,使用读写锁实现线程同步,当线程需要对共享数据进行读操作时,需要先获取读锁,当读取操作完成之后再释放读锁;当线程需要对共享数据进行写操作时,需要先获取到写锁,当写操作完成之后再释放写锁。
当读写锁处于写锁加锁状态时,在这个写锁被解锁之前,所有试图对这个写锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
当读写锁处于读锁加锁状态时,所有试图以读模式对它进行加读锁的线程都可以加锁成功;但是任何以写模式对它进行加写锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的读锁为止。
【注意事项】写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。
2. 读写哪个优先?
2.1 读优先锁
读优先锁希望的是,读锁能被更多的线程持有,以便提高读线程的并发性,它的工作方式是:当读线程 1
先持有了读锁,写线程 2
在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 3
仍然可以成功获取读锁,最后直到读线程 1
和 3
释放读锁后,写线程 2
才可以成功获取写锁。如下图所示:
2.2写优先锁
写优先锁是优先让写线程获取写锁,其工作方式是:当读线程 1
先持有了读锁,写线程 2
在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 3
获取读锁时会失败,于是读线程 3
将被阻塞在获取读锁的操作,这样只要读线程1
释放读锁后,写线程 1
就可以成功获取读锁。
2.3公平读写
读优先锁对于读线程并发性更好,但也不是没有问题。如果一直有读线程获取读锁,那么写线程将永远获取不到写锁,这就造成了写线程一直阻塞的情况。
写优先锁可以保证写线程优先获取写锁,但是如果一直有写线程获取写锁,读线程也会发生一直阻塞的情况。
既然不管优先读锁还是写锁,对方可能会出现一直阻塞的问题,那么就不偏袒任何一方,来一手公平读写,即用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁,先来的自然就要优先获取锁,这样读线程仍然可以并发,也不会出现某一种锁一直阻塞的现象。
3. 应用场景
各操作系统对读写锁的实现各不相同,但当读写锁处于读锁加锁状态,而这时有一个线程试图以写模式获取锁时,该线程会被阻塞;而如果另一线程以读模式获取锁,则会成功获取到锁,对共享资源进行读操作。所以,读写锁非常适合于对共享数据读的次数远大于写的次数的情况。
二、基本操作
1. 读写锁初始化
读写锁与互斥锁的初始化类似,都是有两种方式,一种是动态方式,即通过pthread_rwlock_init()
函数完成初始化;另一种是静态方式,通过宏进行初始化。
1.1 pthread_rwlock_init()
1.1.1 函数说明
在linux
下可以使用man pthread_rwlock_init
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
额……,😥函数声明又是这样,一些资料中使用的另一种声明,就还是使用自己容易理解的吧,反正传入的参数以及格式都是一样的。
【函数说明】该函数是以动态方式初始化一个读写锁。
【函数参数】
rwlock
:pthread_rwlock_t *
类型,指向需要进行初始化操作的读写锁对象。attr
:const pthread_rwlockattr_t *
类型,指向一个pthread_rwlockattr_t
类型对象,该对象用于定义读写锁的属性,若将参数attr
设置为NULL
,则表示将读写锁的属性设置为默认值,在这种情况下其实就等价于PTHREAD_RWLOCK_INITIALIZER
这种方式初始化,而不同之处在于,使用宏不进行错误检查。
【返回值】int
类型,成功返回0
;失败将返回一个非0
的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
1.1.2 使用实例
暂无。
1.2 PTHREAD_RWLOCK_INITIALIZER
1.2.1 函数说明
在linux
下可以使用man 3 pthread_rwlock_init
命令查看该宏的定义格式。
1 | /* Compile and link with -pthread. */ |
【函数说明】该宏用于使用默认属性初始化一个读写锁。
【定义原型】该宏定义在pthread.h
文件中,可以在终端中使用以下命令查看pthread.h
文件位置:
1 | locate pthread.h # 若出现locate 命令未安装之类的,按照提示安装即可 |
定义形式如下:
1 | /* Read-write lock initializers. */ |
【返回值】none
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)PTHREAD_RWLOCK_INITIALIZER
宏已经携带了读写锁的默认属性。
(2)只适用于在定义的时候就直接进行初始化,对于其它情况则不能使用这种方式。
1.2.2 使用实例
暂无。
2. 加锁和解锁
2.1 pthread_mutex_lock()
2.1.1 函数说明
在linux
下可以使用man pthread_rwlock_rdlock
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】该函数以读模式对读写锁上锁,也就是获取读锁(获取不到时,会阻塞等待)。
【函数参数】
rwlock
:pthread_rwlock_t *
类型,指向已经初始化过的读写锁对象。
【返回值】int
类型,用成功时返回0
;失败将返回一个非0
值的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)读写锁处于读模式加锁状态时,其它线程调用pthread_rwlock_rdlock()
函数可以成功获取到锁,如果调用pthread_rwlock_wrlock()
函数则不能获取到锁,从而陷入阻塞等待状态。
(2)当读写锁处于写模式加锁状态时,其它线程调用pthread_rwlock_rdlock()
或pthread_rwlock_wrlock()
函数均会获取锁失败,从而陷入阻塞等待状态。
2.1.2 使用实例
暂无。
2.2 pthread_rwlock_tryrdlock()
2.2.1 函数说明
在linux
下可以使用man pthread_rwlock_tryrdlock
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】该函数以读模式对读写锁上锁,也就是获取读锁(获取不到时,不会阻塞等待,会直接返回)。
【函数参数】
mutex
:pthread_rwlock_t *
类型,指向已经初始化过的读写锁对象。
【返回值】int
类型,用成功时返回0
;失败将返回一个非0
值的错误码;如果目标读写锁已经被其它线程锁住,则调用失败并返回,一般返回的错误码为EBUSY
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】在任何情况下,pthread_rwlock_tryrdlock()
函数都不能被阻塞;它总是要么获得锁,要么失败并立即返回。
2.2.2 使用实例
暂无。
2.3 pthread_rwlock_wrlock()
2.3.1 函数说明
在linux
下可以使用man pthread_rwlock_wrlock
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】该函数以写模式对读写锁上锁,也就是获取写锁(获取不到时,会阻塞等待)。
【函数参数】
rwlock
:pthread_rwlock_t *
类型,指向已经初始化过的读写锁对象。
【返回值】int
类型,用成功时返回0
;失败将返回一个非0
值的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)读写锁处于读模式加锁状态时,其它线程调用pthread_rwlock_rdlock()
函数可以成功获取到锁,如果调用pthread_rwlock_wrlock()
函数则不能获取到锁,从而陷入阻塞等待状态。
(2)当读写锁处于写模式加锁状态时,其它线程调用pthread_rwlock_rdlock()
或pthread_rwlock_wrlock()
函数均会获取锁失败,从而陷入阻塞等待状态。
2.3.2 使用实例
暂无。
2.4 pthread_rwlock_trywrlock()
2.4.1 函数说明
在linux
下可以使用man pthread_rwlock_trywrlock
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】该函数以写模式对读写锁上锁,也就是获取写锁(获取不到时不会阻塞等待,会直接返回)。
【函数参数】
mutex
:pthread_rwlock_t *
类型,指向已经初始化过的读写锁对象。
【返回值】int
类型,用成功时返回0
;失败将返回一个非0
值的错误码;如果目标读写锁已经被其它线程锁住,则调用失败并返回,一般返回的错误码为EBUSY
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】在任何情况下,pthread_rwlock_trywrlock()
函数都不能被阻塞;它总是要么获得锁,要么失败并立即返回。
2.4.2 使用实例
暂无。
2.5 pthread_rwlock_unlock()
2.5.1 函数说明
在linux
下可以使用man pthread_rwlock_unlock
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】该函数用于对读写锁解锁、释放读写锁(读锁或者写锁均可以由此函数释放)。
【函数参数】
mutex
:pthread_mutex_t *
类型,指向已经初始化过的读写对象。
【返回值】int
类型,用成功时返回0
;失败将返回一个非0
值的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.5.2 使用实例
暂无。
3. 销毁读写锁
定义了读写锁之后,当不再需要读写锁时,应该将其销毁。
3.1 pthread_rwlock_destroy()
3.1.1 函数说明
在linux
下可以使用man pthread_rwlock_destroy
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】该函数用于销毁一个不再使用的读写锁。
【函数参数】
mutex
:pthread_rwlock_t *
类型,指向已经初始化过的读写锁对象。
【返回值】int
类型,用成功时返回0
;失败将返回一个非0
值的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
3.1.2 使用实例
暂无。
4. 读写锁使用实例
前边终于把需要使用的函数学习完了,接下来就是一个实例啦。
点击查看实例
1 |
|
程序中的宏macro_rwlock
用于决定是否初始化读写锁,若初始化的话,用什么方式初始化。
- 若
macro_rwlock
为0
,则不使用读写锁。 - 若
macro_rwlock
为1
,则使用函数pthread_rwlock_init()
初始化读写锁。 - 若
macro_rwlock
为2
,则使用宏PTHREAD_RWLOCK_INITIALIZER
初始化读写锁。
在终端执行以下命令编译程序:
1 | gcc test.c -Wall -l pthread # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | This is threadRead thread create,ret=0,tid1=139888351057472 |
然后我们打开test.txt
文件,查看文件内容如下:
1 | II wwrirtite eth rthreeaadWdWrirtiete12 lilinnee |
该文件被四个线程共享,两个线程都是写入,未保护共享文件,导致文件写入出错。
在终端执行以下命令编译程序:
1 | gcc test.c -Wall -l pthread # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | This is threadRead thread create,ret=0,tid1=140066570966592 |
然后我们打开test.txt
文件,查看文件内容如下:
1 | I write threadWrite1 line |
该文件被两个线程共享,两个线程都是写入,使用读写锁保护共享文件,文件写入正常。但是呢,这里其实是有一个问题的,就是我们测试文件为空时,运行程序,程序写线程都会正常写入,但是读线程可能是不会运行的,即便是第二次再运行,读线程可能只会读一部分,然后停止读取,这是为什么呢?还记得前边介绍的读写哪个优先一节把,这就出现了写锁优先的情况,导致读线程一直被阻塞,写线程一直写入的情况,不过这里仅为测试读写锁,暂时不关心这个问题。
在终端执行以下命令编译程序:
1 | gcc test.c -Wall -l pthread # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | This is threadRead thread create,ret=0,tid1=139988447561280 |
然后我们打开test.txt
文件,查看文件内容如下:
1 | I write threadWrite1 line |
该文件被四个线程共享,两个线程都是写入,使用读写锁保护共享文件,文件写入正常。但是呢,这里其实是有一个问题的,就是我们测试文件为空时,运行程序,程序写线程都会正常写入,但是读线程可能是不会运行的,即便是第二次再运行,读线程可能只会读一部分,然后停止读取,这是为什么呢?还记得前边介绍的读写哪个优先一节把,这就出现了写锁优先的情况,导致读线程一直被阻塞,写线程一直写入的情况,不过这里仅为测试读写锁,暂时不关心这个问题。
三、读写锁属性
读写锁与互斥锁类似,也是有属性的。使用pthread_rwlock_init()
函数初始化读写锁时可以设置互斥锁的属性,通过参数attr
指定。如果将参数attr
设置为NULL
,则表示将互斥锁属性设置为默认值。如果不使用默认属性,在调用pthread_rwlock_init()
函数时,参数attr
必须要指向一个pthread_rwlockattr_t
对象,而不能使用NULL
。
1. 属性对象初始化
1.1 pthread_rwlockattr_init()
1.1.1 函数说明
在linux
下可以使用man pthread_rwlockattr_init
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】该函数初始化一个读写锁的属性对象(就是自定义创建的读写锁的各个默认属性)。
【函数参数】
attr
:pthread_rwlockattr_t *
类型,指向需要初始化的读写锁属性对象。
【返回值】int
类型,用成功时返回0
;失败将返回一个非0
值的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
1.1.2 使用实例
暂无。
2. 属性对象销毁
2.1 pthread_rwlockattr_destroy()
2.1.1 函数说明
在linux
下可以使用man pthread_rwlockattr_destroy
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】该函数用于销毁一个不再使用的读写锁属性对象。
【函数参数】
attr
:pthread_rwlockattr_t *
类型,指向已经初始化过且不需要再使用的读写锁属性对象。
【返回值】int
类型,用成功时返回0
;失败将返回一个非0
值的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.1.2 使用实例
暂无。
3. 共享属性
3.1 pthread_rwlockattr_getpshared()
3.1.1 函数说明
在linux
下可以使用man pthread_rwlockattr_getpshared
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
额……,😥又是这样声明的一个函数,使用另一种声明吧,好理解一些。
【函数说明】该函数获取一个读写锁的共享属性。
【函数参数】
attr
:const pthread_rwlockattr_t *
类型,指向已经初始化过的读写锁属性对象。pshared
:int *
类型,将获取的读写锁共享属性保存在参数pshared
所指向的内存中。
【返回值】int
类型,用成功时返回0
;失败将返回一个非0
值的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
3.1.2 使用实例
暂无。
3.2 pthread_rwlockattr_setpshared()
3.2.1 函数说明
在linux
下可以使用man pthread_rwlockattr_setpshared
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】该函数设置一个读写锁的共享属性。
【函数参数】
attr
:const pthread_rwlockattr_t *
类型,指向已经初始化过的读写锁属性对象。pshared
:int
类型,将读写锁共享属性设置为参数pshared
所指向的内存中的值。
点击查看 pshared 可取的值
PTHREAD_PROCESS_SHARED | 共享读写锁。该读写锁可以在多个进程中的线程之间共享; |
PTHREAD_PROCESS_PRIVATE | 私有读写锁。只有本进程内的线程才能够使用该读写锁,这是读写锁共享属性的默认值。 |
【返回值】int
类型,用成功时返回0
;失败将返回一个非0
值的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
3.2.2 使用实例
暂无。
3.3共享属性设置实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall -lpthread # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | rwlock pshared:0 |