最近在写代码的时候,处理多线程间数据同步时,用到了读写锁rwlock。在多线程同步中,更常用到的是互斥量mutex,那rwlock和mutex有什么不同和优劣呢?
首先,一个常见的误区是,认为在读多写少的情况下,rwlock的性能一定要比mutex高。实际上,rwlock由于区分读锁和写锁,每次加锁时都要做额外的逻辑处理(如区分读锁和写锁、避免写锁“饥饿”等等),单纯从性能上来讲是要低于更为简单的mutex的;但是,rwlock由于读锁可重入,所以实际上是提升了并行性,在读多写少的情况下可以降低时延。
我们可以做如下实验验证一下:
#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);
}
#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++
待业青年
转载请注明出处: blog.guoyb.com/2018/02/11/…