名不符实的读写锁

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

左侧是一种经典的解法,对数据整个操作加锁。为了一个写数据线程,于将所有读线程也进行加锁显然有点浪费了。于是提出读写锁(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都要好 (最后一个算法):

详细的不展开了。另外有一些更为通用的方式,包括平衡读写的吞吐的问题,称为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

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

时间: 2024-10-25 21:43:16

名不符实的读写锁的相关文章

非一致性内存访问的读写锁

原文地址,译文地址,译者: 李杰聪,校对:郑旭东 原文作者: Irina Calciu         Brown University        irina@cs.brown.edu Dave Dice          Oracle Labs             dave.dice@oracle.com Yossi Lev           Oracle Labs             yossi.lev@oracle.com Victor Luchangco    Oracle

读写锁和缓存类实现

提高性能 读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,写锁与写锁互斥,这是由JVM控制的,我们只需要上好相应的锁即可.如果代码只读数据,可以很多人同时读,但不能同时写,那就上读锁:如果代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁.总之,读的时候上读锁,写的时候上写锁. import java.util.Random; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.

Java中的读/写锁

原文链接 作者:Jakob Jenkov 译者:微凉 校对:丁一 相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些.假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁.在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源.但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存).这就需要一个读/写锁来解决这个问题

php的session读写锁例子

先看一个例子,功能: 1.点击页面中一个按钮,ajax执行php,php中用session记录执行到哪一步. 2.使用ajax轮询另一个php,获取session中数据,输出执行到哪一步. session.html 调用php执行,并输出执行到第几步 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

UNIX环境高级编程:线程同步之读写锁及属性

读写锁和互斥量(互斥锁)很类似,是另一种线程同步机制,但不属于POSIX标准,可以用来同步同一进程中的各个线程.当然如果一个读写锁存放在多个进程共享的某个内存区中,那么还可以用来进行进程间的同步, 互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁.读写锁可以有三种状态:读模式下的加锁状态,写模式下的加锁状态,不加锁状态. 一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁. 当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁(读或写

并发数据结构: .NET Framework中提供的读写锁

在多线程编程时,开发人员经常会遭遇多个线程读写某个资源的情况.这就需要进行线程同步来保证 线程安全.一般情况下,我们的同步措施是使用锁机制.但是,假如线程只对资源进行读取操作,那么根 本不需要使用锁:反之,假如线程只对资源进行写入操作,则应当使用互斥锁(比如使用 Monitor 类等) .还有一种情况,就是存在多个线程对资源进行读取操作,同时每次只有一个线程对资源进行独占写入操 作.这正是本文主题--读写锁的用武之地.ReaderWriterLock 类 .NET Framework BCL 在

可扩展的快速读写锁

原文链接(需翻墙) ,译文链接,  译者:中麦-张军 ,校对:梁海舰 介绍 读写锁是一种允许多个线程并发地访问一个或一组资源的并发结构,这意味着在实践中如果你有一个或一组几乎是以只读方式访问时,可以考虑使用读写锁(后文统称为RWLocks)来保护它们: Java在java.util.concurrent.locks.ReentrantReadWriteLock中提供了一个很好的RWLocks,是由Doug Lea创建,它有很多特性如:重入,公平锁,锁降级等: 如果你是一个无法摆脱PThreads

Java多线程编程中线程锁与读写锁的使用示例_java

线程锁LockLock  相当于 当前对象的 Synchronized import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /* * Lock lock = new ReentrantLock(); * lock.lock(); lock.unLock(); * 类似于 synchronized,但不能与synchronized 混用 */ public class Lo

java 可重入读写锁 ReentrantReadWriteLock 详解

读写锁 ReadWriteLock读写锁维护了一对相关的锁,一个用于只读操作,一个用于写入操作.只要没有writer,读取锁可以由多个reader线程同时保持.写入锁是独占的.互斥锁一次只允许一个线程访问共享数据,哪怕进行的是只读操作:读写锁允许对共享数据进行更高级别的并发访问:对于写操作,一次只有一个线程(write线程)可以修改共享数据,对于读操作,允许任意数量的线程同时进行读取.与互斥锁相比,使用读写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用--即在同一