读者写者问题与读写锁¶
约 1709 个字 114 行代码 预计阅读时间 7 分钟
何为读者写者问题¶
读者写者问题是并发编程中的一个经典问题,它描述了如何在多个线程(或进程)同时访问共享资源时,允许多个读者并发读取但要求写者独占访问的情况。也就是说,在没有写操作时,多个线程可以同时读取共享数据,而写操作则必须保证在写入期间没有其他读者或写者在访问共享资源
读者写者问题与生产消费模型¶
读者写者问题非常类似于前面的生产消费模型,其也存在「321」原则:
- 3种关系:读者与读者、写者与写者和读者与写者
- 2种角色:读者和写者
- 1个交易场所
其中写者和写者之间的的关系与读者和写者之间的关系与生产消费模型一致,分别是互斥与互斥和同步,唯一不同的就是读者和读者之间的关系并不同于生产消费模型中消费者和消费者之间的关系
在生产消费模型中,每一个消费者都会对交易场所存在的数据进行取出,如果不使用互斥,那么肯定会存在两个线程访问到同一个数据导致同一个数据被多次取出的问题,所以需要使用互斥来避免。但是在读者写者问题中,读者仅仅是读交易场所的数据,这个行为并不会影响已有数据的个数,所以可以允许多个读者并发访问共享资源。这就是生产消费模型和读者写者问题二者最主要的区别
读者和写者如何完成同步与互斥¶
读者和写者之间的配合可以按照下面的伪代码去理解:
C++ | |
---|---|
1 2 3 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
C++ | |
---|---|
1 2 3 4 |
|
读写锁¶
在编写多线程的时候,有一种情况是十分常见的:有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低当前程序的效率,所以此时就需要用到读者写者问题的逻辑,即读写锁。在读者写者问题中,读写锁有下面的几种行为:
当前锁状态 | 读锁请求 | 写锁请求 |
---|---|---|
无锁 | 可以 | 可以 |
读锁 | 可以 | 阻塞 |
写锁 | 阻塞 | 阻塞 |
在上面的表格中,如果没有锁,那么读者和写者就是正常的访问共享资源,此时肯定会涉及到线程安全问题;如果当前是读锁,那么根据读者写者问题的特点:读者可以并发访问,所以就算有多个读者,这些读者也不会因为有一个读者已经正在读而被阻塞在获取锁的部分;如果当前是写锁,那么为了保证同一个时刻只有一个线程可以写入,就必须保证一旦有一个线程持有写者锁,其他线程必须阻塞等待,这样才可以防止并发写入导致数据出现问题
在实际开发中,因为pthread
库本身已经封装了相关的接口,所以不需要程序员手动实现读写锁的逻辑,常见的接口如下:
设置读者和写者优先权
在读者写者问题中,如果写者先写,那么如果写者多,读者少,就会有极大概率出现读者饥饿问题。同样地,如果读者先读,那么如果读者多,写者少,就会有极大概率出现写者饥饿问题。可见,不论是那一方优先,总会有一方可能存在饥饿问题,所以读者写者问题中,「有一方可能存在饥饿问题」是读者写者问题的特性而不是一个明显问题
在Linux中想指定哪一方优先,就可以使用pthread_rwlockattr_setkind_np
接口,该接口原型如下:
C | |
---|---|
1 |
|
在上面的接口中,第一个参数表示读写锁属性,第二个参数表示标记,有3种选择:
PTHREAD_RWLOCK_PREFER_READER_NP
:读者优先,也是Linux系统的默认值PTHREAD_RWLOCK_PREFER_WRITER_NP
:写者优先(但是可能存在问题,导致与第一种情况一样)PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP
:写者优先,但是写者不能递归加锁
初始化读写锁
在Linux中,初始化读写锁可以使用pthread_rwlock_init
,其原型如下:
C | |
---|---|
1 2 |
|
该接口第一个参数表示读写锁类型,第二个参数表示读写锁属性
读者加锁
在Linux中,使用pthread_rwlock_rdlock
接口对读者进行加锁,其原型如下:
C | |
---|---|
1 |
|
写者加锁
在Linux中,使用pthread_rwlock_wrlock
接口对写者进行加锁,其原型如下:
C | |
---|---|
1 |
|
解锁
虽然读者加锁和写者加锁是两个接口,但是解锁只有一个接口:pthread_rwlock_unlock
,其原型如下:
C | |
---|---|
1 |
|
销毁读写锁
使用pthread_rwlock_destroy
接口对读写锁进行销毁,其原型如下:
C | |
---|---|
1 |
|
使用示例¶
读写锁使用示例参考下面的代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
|
上面的代码直接运行会发现读者一直在读,而写者一直没有机会写导致出现饥饿问题,这也符合Linux默认读者优先的特点
读者优先与写者优先¶
读者优先(Reader-Preference)
在这种策略中,系统会尽可能多地允许多个读者同时访问资源(比如共享文件或数据),而不会优先考虑写者。这意味着当有读者正在读取时,新到达的读者会立即被允许进入读取区,而写者则会被阻塞,直到所有读者都离开读取区。读者优先策略可能会导致写者饥饿(即写者长时间无法获得写入权限),特别是当读者频繁到达时
写者优先(Vriter-Preference)
在这种策略中,系统会优先考虑写者。当写者请求写入权限时,系统会尽快地让写者进入写入区,即使此时有读者正在读取。这通常意味着一旦有写者到达,所有后续的读者都会被阻塞,直到写者完成写入并离开写入区。写者优先策略可以减少写者等待的时间,但可能会导致读者饥饿(即读者长时间无法获得读取权限),特别是当写者频繁到达时