名不符實的讀寫鎖

有一種單一寫線程,多個讀線程併發的場景,比如測量數據的讀取與更新,消費者會比較多,生產者只有一個。以下圖爲例:
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

有空再寫篇關於多線程算法選擇的文檔!

發佈了220 篇原創文章 · 獲贊 29 · 訪問量 174萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章