名不符实的读写锁

有一种单一写线程。多个读线程并发的场景,比方測量数据的读取与更新,消费者会比較多。生产者仅仅有一个。下面图为例:

左側是一种经典的解法,对数据整个操作加锁。为了一个写数据线程,于将全部读线程也进行加锁显然有点浪费了。于是提出读写锁(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-12 11:25:52

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

Java并发程序设计(15)并发锁之读写锁(续二)写锁降级

1.1.1. 读写锁应用之三写锁降级 ReentrantReadWriteLock还具有写锁降级的特点,而这跟可重入性有一些关系. (1)持有写锁时可以降级为读锁. (2)持有读锁时不能升级为写锁. ReentrantReadWriteLock和ReentrantLock相似的是都有一个特点,就是可重入.可重入指已经获取到锁的线程可以再次获取锁,保证lock和unlock的次数相同即可. package com.test.concurrence; import java.util.Random;

ReentrantReadWriteLock读写锁的使用2

本文可作为传智播客<张孝祥-Java多线程与并发库高级应用>的学习笔记. 这一节我们做一个缓存系统. 在读本节前 请先阅读 ReentrantReadWriteLock读写锁的使用1 第一版 public class CacheDemo { private Map<String, Object> cache = new HashMap<String, Object>(); public static void main(String[] args) { CacheDem

深入浅出 Java Concurrency (13): 锁机制 part 8 读写锁 (ReentrantReadWriteLock) (1)[转]

从这一节开始介绍锁里面的最后一个工具:读写锁(ReadWriteLock). ReentrantLock 实现了标准的互斥操作,也就是一次只能有一个线程持有锁,也即所谓独占锁的概念.前面的章节中一直在强调这个特点.显然这个特点在一定程度上面减低了吞吐量,实际上独占锁是一种保守的锁策略,在这种情况下任何“读/读”,“写/读”,“写/写”操作都不能同时发生.但是同样需要强调的一个概念是,锁是有一定的开销的,当并发比较大的时候,锁的开销就比较客观了.所以如果可能的话就尽量少用锁,非要用锁的话就尝试看能

锁,同步,可重入锁,读写锁(转)

1.synchronized 把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility). 1.1 原子性 原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护.从而防止多个线程在更新共享状态时相互冲突. 1.2 可见性 可见性则更为微妙,它要对付内存缓存和编译器优化的各种反常行为.它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 . 作用:

linux 读写锁应用实例

转自:http://blog.csdn.net/dsg333/article/details/22113489 /*使用读写锁实现四个线程读写一段程序的实例,共创建了四个新的线程,其中两个线程用来读取数据,另外两个线程用来写入数据.在任意时刻,如果有一个线程在写数据,将阻塞所有其他线程的任何操作.*/#include <errno.h>#include <pthread.h>#include <stdio.h>#include <stdlib.h>#incl

读写锁ReadWriteLock和缓存实例

读写锁:多个读锁不互斥,读锁与些锁互斥,写锁与写锁互斥.即:读的时候不允许写,写的时候不允许读,可以同时读. synchronized关键字和普通的Lock构造的锁,会造成读与读之间的互斥,因此读写锁可提高性能. 例子1:三个线程同时对一个共享数据进行读写. import java.util.Random; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantRea

Linux多线程实践(6) --Posix读写锁解决读者写者问题

Posix读写锁 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthre

深入浅出 Java Concurrency (14): 锁机制 part 9 读写锁 (ReentrantReadWriteLock) (2)[转]

这一节主要是谈谈读写锁的实现. 上一节中提到,ReadWriteLock看起来有两个锁:readLock/writeLock.如果真的是两个锁的话,它们之间又是如何相互影响的呢? 事实上在ReentrantReadWriteLock里锁的实现是靠java.util.concurrent.locks.ReentrantReadWriteLock.Sync完成的.这个类看起来比较眼熟,实际上它是AQS的一个子类,这中类似的结构在CountDownLatch.ReentrantLock.Semapho

数据库读写锁的实现(C++)

一.基本概念 在数据库中,对某数据的两个基本操作为写和读,分布有两种锁控制:排它锁(X锁).共享锁(S锁). 排它锁(x锁):若事务T对数据D加X锁,则其它任何事务都不能再对D加任何类型的锁,直至T释放D上的X锁: 一般要求在修改数据前要向该数据加排它锁,所以排它锁又称为写锁. 共享锁(s锁):若事务T对数据D加S锁,则其它事务只能对D加S锁,而不能加X锁,直至T释放D上的S锁: 一般要求在读取数据前要向该数据加共享锁, 所以共享锁又称读锁. 程序所收到的请求包括以下五种:Start.End.X