读写锁的性能一定更好吗


最近在写代码的时候,处理多线程间数据同步时,用到了读写锁rwlock。在多线程同步中,更常用到的是互斥量mutex,那rwlock和mutex有什么不同和优劣呢?

首先,一个常见的误区是,认为在读多写少的情况下,rwlock的性能一定要比mutex高。实际上,rwlock由于区分读锁和写锁,每次加锁时都要做额外的逻辑处理(如区分读锁和写锁、避免写锁“饥饿”等等),单纯从性能上来讲是要低于更为简单的mutex的;但是,rwlock由于读锁可重入,所以实际上是提升了并行性,在读多写少的情况下可以降低时延。

我们可以做如下实验验证一下:

test_mutex.cc
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
#include <pthread.h>
#include <iostream>
#include <unistd.h>

pthread_mutex_t mutex;
int i = 0;

void *thread_func(void* args) {
int j;
for(j=0; j<10000000; j++) {
pthread_mutex_lock(&mutex);
for(int k=0; k<1; k++) {
int t = i;
t++;
}
pthread_mutex_unlock(&mutex);
}
pthread_exit((void *)0);
}

int main(void) {
pthread_t id1;
pthread_t id2;
pthread_t id3;
pthread_t id4;
pthread_mutex_init(&mutex, NULL);
pthread_create(&id1, NULL, thread_func, (void *)0);
pthread_create(&id2, NULL, thread_func, (void *)0);
pthread_create(&id3, NULL, thread_func, (void *)0);
pthread_create(&id4, NULL, thread_func, (void *)0);
pthread_join(id1, NULL);
pthread_join(id2, NULL);
pthread_join(id3, NULL);
pthread_join(id4, NULL);
pthread_mutex_destroy(&mutex);
}
test_rwlock.cc
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
#include <pthread.h>
#include <iostream>
#include <unistd.h>

pthread_rwlock_t rwlock;
int i = 0;

void *thread_func(void* args) {
int j;
for(j=0; j<10000000; j++) {
pthread_rwlock_rdlock(&rwlock);
for(int k=0; k<1; k++) {
int t = i;
t++;
}
pthread_rwlock_unlock(&rwlock);
}
pthread_exit((void *)0);
}

int main(void) {
pthread_t id1;
pthread_t id2;
pthread_t id3;
pthread_t id4;
pthread_rwlock_init(&rwlock, NULL);
pthread_create(&id1, NULL, thread_func, (void *)0);
pthread_create(&id2, NULL, thread_func, (void *)0);
pthread_create(&id3, NULL, thread_func, (void *)0);
pthread_create(&id4, NULL, thread_func, (void *)0);
pthread_join(id1, NULL);
pthread_join(id2, NULL);
pthread_join(id3, NULL);
pthread_join(id4, NULL);
pthread_rwlock_destroy(&rwlock);
}

可以看到,这两种情况下,基本没有什么计算逻辑,线程所做的事情就是在不断的加锁、解锁。

mutex的性能:

real    0m2.363s
user    0m1.904s
sys     0m3.592s

rwlock的性能:

real    0m5.157s
user    0m5.932s
sys     0m10.660s

可以看到,单纯从锁的性能上来看,mutex是要优于rwlock的。


上面只是一个理想情况,正常情况下,在临界区内,往往都是需要针对共享资源做一些计算/IO操作的。我们将上面代码中的外层循环和内层循环改为分别改为1000次和10000次,以模仿有一定计算量的情况。测试结果如下:

mutex的性能:

real    0m0.102s
user    0m0.024s
sys     0m0.088s

rwlock的性能:

real    0m0.045s
user    0m0.112s
sys     0m0.012s

注意到,这时从real上看,rwlock已经优于mutex了。另外,对于mutex,user+sys基本等于real,可见其基本没有带来什么并行性;而rwlock的user时间就要长于real,可见内层循环部分的代码,是由一定的并行性的。


但是这个时候,观察CPU的使用率,基本都在满负荷运转。

我们在内层循环结束之后,用usleep(1000)模拟一段IO等待时间。这种情况下的测试结果如下:

mutex的性能:

real    0m7.987s
user    0m0.200s
sys     0m0.412s

rwlock的性能:

real    0m1.632s
user    0m0.112s
sys     0m0.028s

可以看到,rwlock这时的表现更好,可重入性充分利用了线程在IO等待的时间提高了并行性。


上面的几个例子其实是想说明,对于这种情况,最好的办法还是针对业务场景,做一次性能测试,以实测结果为准绳来选择具体使用哪一种锁。

但是,rwlock有一个非常大的隐患,这个隐患也是由于读锁可重入带来的:读锁的可重入性前提条件是在读锁控制的临界区内,对共享资源只有读操作而没有写操作。然而,对于程序的维护者(非开发者)来说,很容易就忽视了这一点(想想你自己在接手一份别人写的代码时,会特别关注某段代码是rwlock控制的还是mutex控制的吗?),从而在读锁的范围内引入写操作。我认为这是使用读写锁时需要考虑的最严重的一个问题。


推荐阅读:

scala模式匹配的一个问题
打通Python和C++
待业青年

转载请注明出处: http://blog.guoyb.com/2018/02/11/rwlock/

欢迎使用微信扫描下方二维码,关注我的微信公众号TechTalking,技术·生活·思考:
后端技术小黑屋

Comments