名不符实的读写锁

简介: 有一种单一写线程,多个读线程并发的场景,比如测量数据的读取与更新,消费者会比较多,生产者只有一个。

有一种单一写线程,多个读线程并发的场景,比如测量数据的读取与更新,消费者会比较多,生产者只有一个。以下图为例:
diagrams
左侧是一种经典的解法,对数据整个操作加锁。为了一个写数据线程,于将所有读线程也进行加锁显然有点浪费了。于是提出读写锁(Reader/Writer Lock), 即使是使用了读写锁,其本质也是一样的,而且在POSIX下的pthread它的内部实现是基于mutex,所以它的开销更大。如果没有很重的读操作来抵消它引入的开销,反而会引起性能的下降。已经多组测试数据来证明这一点。我自己也做了验证,得到数据如下 (单个写线程,20个读线程),使用读写锁反而比使用mutex要慢。详细可以参考两个链接:
* Mutex or Reader Writer Lock
* Multi-threaded programming: efficiency of locking

这一类问题,在数据库领域有一类解决方案,被称为Multiversion Concurrency Control, 其目的是以增加数据复本保证用户每一次使用都可以用到完整的数据,但不一定是最新的数据。再简化一点,其思想就是建立一个数据复本,专门用于写。当数据完全准备好后,切换出来供其它线程读。原本的数据就转为下一次写使用。 即上图中右侧所示的方式。
以这个方案,只要对Writing/Reading的处理加锁就可以了。这样测试出来的性能开销因为加锁的处理时间极短,较一般Mutex和Reader/Writer Lock都要好 (最后一个算法):
Measurement

详细的不展开了。另外有一些更为通用的方式,包括平衡读写的吞吐的问题,称为Spin Buffer,有兴趣可以进一步研究。

附源代码如下供参考:

#include <pthread.h>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <ctime>

// #define USE_MUTEX
// #define USE_RW_LOCK

// X + Y = 0
typedef struct _Data{
  int x;
  int y;
} Data;

namespace {
  Data globalData[2] = {{1,-1}, {1,-1}};
  int WriteIndex = 0;
  int ReadingIndex = 1;

  float globalReadingTimeCost = 0.0f;

#ifdef USE_MUTEX
  pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
#endif
#ifdef USE_RW_LOCK
  pthread_rwlock_t rwlock;
#endif
  const int thread_number = 20;
}

void* write_thread(void* param) {
  clock_t begin_time = std::clock();
  for(int i=1; i<=1000; i++) {
    globalData[WriteIndex].x = i;
    globalData[WriteIndex].y = -1 * i;
    usleep(1);

#ifdef USE_MUTEX
    pthread_mutex_lock(&mutex);
#endif
#ifdef USE_RW_LOCK
    pthread_rwlock_rdlock(&rwlock);
#endif
    ReadingIndex = WriteIndex;
    WriteIndex = (WriteIndex + 1) % 2;
#ifdef USE_MUTEX
    pthread_mutex_unlock(&mutex);
#endif
#ifdef USE_RW_LOCK
    pthread_rwlock_unlock(&rwlock);
#endif
    usleep(600);
  }
  std::cout<< "[Writing Thread]" << float( std::clock () - begin_time ) /  CLOCKS_PER_SEC * 1000 << std::endl;
  return NULL;
}

void* read_thread(void* param) {
  clock_t begin_time = std::clock();
  for(int i=1; i<=20000; i++) {
#ifdef USE_MUTEX
    pthread_mutex_lock(&mutex);
#endif
#ifdef USE_RW_LOCK
    pthread_rwlock_wrlock(&rwlock);
#endif
    int index = ReadingIndex;
#ifdef USE_MUTEX
    pthread_mutex_unlock(&mutex);
#endif
#ifdef USE_RW_LOCK
    pthread_rwlock_unlock(&rwlock);
#endif

    int x = globalData[index].x;
    int y = globalData[index].y;
    if (x + y != 0) {
      std::cout << std::endl << "Wrong data:" << x << "," << y << std::endl;
    }

    usleep(3);
  }
  std::cout<< "[Reading Thread]" << float( std::clock () - begin_time ) /  CLOCKS_PER_SEC * 1000 << std::endl;
  return NULL;
}


int main(void) {
  int ret = 0;
  pthread_t id[thread_number];

#ifdef USE_RW_LOCK
  pthread_rwlock_init(&rwlock, NULL);
#endif

  clock_t begin_time = std::clock();
  // One writing thread
  ret = pthread_create(&id[0], NULL, write_thread, NULL);
  if (ret) {
    std::cout << "Failed to launch writing thread." << std::endl;
    return -1;
  }
  // Four reading threads
  for (int i=1; i<thread_number; i++) {
    pthread_create(&id[i], NULL, read_thread, NULL);
  }

  for (int i=0; i<=thread_number; i++) {
    pthread_join(id[i], NULL);
  }

  std::cout<< "Cost:" << float( std::clock () - begin_time ) /  CLOCKS_PER_SEC * 1000 << std::endl;
  return 0;
}

使用如下方式编译测试:

g++ -std=c++11 -DUSE_MUTEX thread.cc -lpthread -o thread

有空再写篇关于多线程算法选择的文档!

目录
相关文章
ReentrantReadWriteLock读写锁
ReentrantReadWriteLock读写锁
|
3月前
|
算法 调度
锁机制和互斥量有什么不同
【10月更文挑战第17天】锁机制和互斥量有什么不同
|
3月前
|
缓存 数据库
读写锁和互斥锁的区别
【10月更文挑战第6天】
117 1
|
8月前
|
缓存 测试技术
ReentrantReadWriteLock 读写锁
ReentrantReadWriteLock 读写锁
54 0
|
5月前
|
Java
JUC(11)各种锁的理解(公平锁、可重入锁、自旋锁、死锁)
这篇文章介绍了Java并发包中的各种锁机制,包括公平锁与非公平锁、可重入锁、自旋锁以及死锁的概念、实现和示例,以及如何使用jps和jstack工具来检测和诊断死锁问题。
|
8月前
|
监控
多线程并发之读写锁(ReentranReadWriteLock&ReadWriteLock)使用详解
多线程并发之读写锁(ReentranReadWriteLock&ReadWriteLock)使用详解
135 0
|
安全 Java 测试技术
读写锁还不会用StampedLock就Out了
读写锁还不会用StampedLock就Out了
147 0
读写锁还不会用StampedLock就Out了
|
缓存 Oracle 关系型数据库
可重入读写锁ReentrantReadWriteLock的使用详解
ReentrantReadWriteLock是一把可重入读写锁,这篇文章主要是从使用的角度帮你理解,希望对你有帮助。
250 0
可重入读写锁ReentrantReadWriteLock的使用详解
|
Java Linux 测试技术
如何理解互斥锁、条件变量、读写锁以及自旋锁?
如何理解互斥锁、条件变量、读写锁以及自旋锁?
499 0
如何理解互斥锁、条件变量、读写锁以及自旋锁?
|
uml
除了读写锁,JUC 下面还有个 StampedLock!还不过来了解一下么?
在了解完 ReentrantLock 和 ReentrantReadWriteLock 之后,惊奇的发现 JUC 下还有一个 StampedLock 。 查阅资料发现是 JDK8 新增的一个锁。现在已经 JDK15 了,原谅我的孤陋寡闻,实在是业务开发中用的太少。那行吧,赶紧来看一下 StampedLock 到底是什么?为什么有了 ReentrantLock 和 ReentrantReadWriteLock 之后还要设计一个 StampedLock ?
131 0