【Liux系统编程】读写锁


读写锁

读写锁从字面意思我们也可以知道,它由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。

所以,读写锁适用于能明确区分读操作和写操作的场景。

读写锁的工作原理是:

  • 当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为「读锁」是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。
  • 但是,一旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。

所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。

知道了读写锁的工作原理后,我们可以发现,读写锁在读多写少的场景,能发挥出优势。

另外,根据实现的不同,读写锁可以分为「读优先锁」和「写优先锁」。

读优先锁期望的是,读锁能被更多的线程持有,以便提高读线程的并发性,它的工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取读锁。如下图:

 而写优先锁是优先服务写线程,其工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁。如下图:

 

归纳总结:

 1. 函数原型:

#include 
pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);  // 初始化读写锁
pthread_rwlock_destroy(pthread_rwlock_t *rwlock);    // 销毁读写锁

2. 函数原型:

#include 
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 读锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);  // 写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // 解锁

【 问题描述】程序 trainticket 中,有 100 个线程,其中 90 个线程是查余票数量的,只有 10 个线程抢票,每个线程一次买 10 张票。初始状态下一共有 1000 张票。因此执行完毕后,还会剩下 900 张票。
程序 trainticket 在运行的时候需要传入参数,即:

    参数 0:表示不加任何锁
    参数 1:表示使用读写锁
    参数 2:表示使用互斥量

代码实现:

 1 #include 
 2 #include 
 3 #include 
 4 #include <string.h>
 5 #include 
 6  
 7 struct Ticket 
 8 {
 9     int remain;              // 余票数,初始化为 1000
10     pthread_rwlock_t rwlock; // 读写锁
11     pthread_mutex_t mlock;   // 互斥锁,主要是为了和读写锁进行对比
12 }ticket;
13  
14 // 通过命令行传参数来取得这个值,用来控制到底使用哪一种锁
15 // 0:不加锁 1:加读写锁 2:加互斥锁
16 int lock = 0;
17  
18 void* query(void* arg)  //查票线程
19 {
20     int name = (int)arg;
21     sleep(rand() % 5 + 1);
22     if (lock == 1)
23         pthread_rwlock_rdlock(&ticket.rwlock); // 读模式加锁
24     else if (lock == 2)
25         pthread_mutex_lock(&ticket.mlock);
26  
27     int remain = ticket.remain;
28     sleep(1);
29     printf("%03d query: %d\n", name, remain);
30  
31     if (lock == 1)
32         pthread_rwlock_unlock(&ticket.rwlock);
33     else if (lock == 2)
34         pthread_mutex_unlock(&ticket.mlock);
35  
36     return NULL;
37 }
38  
39  
40 void* buy(void* arg)  // 抢票线程
41 {
42     int name = (int)arg;
43  
44     if (lock == 1)
45         pthread_rwlock_wrlock(&ticket.rwlock); // 写模式加锁
46     else if (lock == 2)
47         pthread_mutex_lock(&ticket.mlock);
48  
49     int remain = ticket.remain;
50     remain -= 10; // 一次买 10 张票
51     sleep(1);
52     ticket.remain = remain;
53     printf("%03d buy 10 tickets\n", name);
54  
55     if (lock == 1)
56         pthread_rwlock_unlock(&ticket.rwlock);
57     else if (lock == 2)
58         pthread_mutex_unlock(&ticket.mlock);
59  
60     sleep(rand() % 5 + 2);
61     return NULL;
62 }
63  
64 int main(int argc, char* argv[]) 
65 {
66     lock = 0;
67     if (argc >= 2) 
68         lock = atoi(argv[1]);
69  
70     int names[100];
71     pthread_t tid[100];
72  
73     int i;
74     for (i = 0; i < 100; ++i) 
75         names[i] = i;
76     ticket.remain = 1000;
77     printf("remain ticket = %d\n", ticket.remain);
78  
79     pthread_rwlock_init(&ticket.rwlock, NULL);
80     pthread_mutex_init(&ticket.mlock, NULL);
81  
82     for (i = 0; i < 100; ++i) {
83         if (i % 10 == 0)
84             pthread_create(&tid[i], NULL, buy, (void*)names[i]);
85         else
86             pthread_create(&tid[i], NULL, query, (void*)names[i]);
87     }
88  
89     for (i = 0; i < 100; ++i) 
90         pthread_join(tid[i], NULL);
91     pthread_rwlock_destroy(&ticket.rwlock);
92     pthread_mutex_destroy(&ticket.mlock);
93     printf("remain ticket = %d\n", ticket.remain);
94     return 0;
95 }

参考资料

1. 面试官:你说说互斥锁、自旋锁、读写锁、悲观锁、乐观锁的应用场景