微信号:TechTalking

介绍:这可能是最接地气的技术分享公众号,我愿在这里与你们分享成长路上的点点滴滴. 关注各类Linux相关后端技术,关爱程序员的生活,与你分享我的思考. 技术·生活·思考

读写锁的性能一定更好吗

2018-02-11 21:45 茄仁波切

点击上方蓝色文字“后端技术小黑屋”,关注茄仁波切的公众号吧~



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

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

我们可以做如下实验测试一下:

test_mutex.cc

#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

#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中的传名调用

面向数据编程

如何做好一次优化与重构


题图:gregstevens

授权:CC0协议



 
后端技术小黑屋 更多文章 狗年快乐 面向数据编程 scala中的传名调用 2017年下半年书单 广深路上
猜您喜欢 [揭秘]双11幕后的天猫技术 所有文章(62篇) + 所有代码!/ 计算机程序的思维逻辑 编译,性能 逆向 AWS API 设计 闲话女程序员