zoukankan      html  css  js  c++  java
  • 读写锁

    上周做性能调优的时候,发现一个测并发读写的场景数据很奇怪。

    场景是测一个写线程加不同数量的读线程时的读写QPS,结果发现数据大致是下面的样子:

    写线程数    读线程数    写QPS    读QPS
    1          1          4000     40
    1          5          3000     10000
    1          10         3000     20000
    ...

    代码大致是这样子的:

    // 写线程
        ReadWriteLockGuard lock(mLock, 'w');
        // do something...
    
    // 读线程
        ReadWriteLockGuard lock(mLock, 'r');
        // do something...

    从这段代码看来,当读写线程是1:1时,应该是两个线程轮流抢锁才对,但QPS却显示出写线程抢到锁的次数是读线程的100倍!

    于是我在读写线程的代码中都加一行打印,来看读写线程抢锁的情况:

    // 写线程
        ReadWriteLockGuard lock(mLock, 'w');
        // do something...
        cout << "w" << endl;
    
    // 读线程
        ReadWriteLockGuard lock(mLock, 'r');
        // do something...
        cout << "r" << endl;

    结果很出乎我的意料:

    r
    w
    w
    w
    ...
    w
    r
    w
    w
    ...

    总之写线程连续抢到若干次锁后,可怜的读线程才抢到一次锁。

    很奇怪的现象。

    我之前对读写锁抢锁流程的理解:

    1. 如果当前没有线程持有锁,那么第一个去抢锁的活动线程会拿到锁;
    2. 如果当前持有者释放锁,那么所有排队的线程会进行抢锁;
    3. 排队的线程中有等写锁的线程时,申请读锁会阻塞(写锁优先)。

    现在看来这个理解是有问题的,无法解释这一现象。

    和同事交流了一下,读写锁抢锁流程可能是这样的:

    1. 如果有线程申请锁阻塞,会首先调用SpinLock一会,之后如果还是没抢到锁,那么内核将其设置为睡眠状态,并加入等待队列;
    2. 当前持有线程释放锁后,内核将所有等待队列中的睡眠线程唤醒,加入调度队列;
    3. 进入调度队列的竞争线程在被调度运行后,开始抢锁。

    从这个流程来看,我遇到的这种情况可以这么解释:

    1. 读线程首先运行,抢到锁;
    2. 因为是写优先,在读线程结束后锁肯定会让给写线程;
    3. 写线程释放锁后,读线程被唤醒,此时还处于等待状态,未运行,不能抢锁;
    4. 写线程没有睡眠,重新抢锁,此时没有写优先的影响,成功抢到锁;
    5. 读线程开始运行,抢锁失败,重新睡眠。

    在读线程增多以后,写线程释放锁后就不一定能抢到锁了,因此会有一定的时间在睡眠,这样进一步增大了读线程抢到锁的概率,因此就观察到读的QPS猛涨的情况。

    上面的猜测还未得到验证,有空还是得看看pthread_read_write_lock的实现啊。

    -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    之所以要“慎用”,是因为能够利用读写锁真正达到想要的效果的情景不多,如果情景使用不正确,使用的效率还不如一般的锁。而且使用读写锁有一个非常容易的错误,在读锁递归使用时(重入时)如果有写锁lock,很多的实现版本上会发生死锁。下面详细说明。

    Be wary of readers/writer locks. If there is a novice error when trying to break up a lock, it is this: seeing that a data structure is frequently accessed for reads and infrequently accessed for writes, one may be tempted to replace a mutex guarding the structure with a readers/writer lock to allow for concurrent readers. This seems reasonable, but unless the hold time for the lock is long, this solution will scale no better (and indeed, may scale worse) than having a single lock. Why? Because the state associated with the readers/writer lock must itself be updated atomically, and in the absence of a more sophisticated (and less space-efficient) synchronization primitive, a readers/writer lock will use a single word of memory to store the number of readers. Because the number of readers must be updated atomically, acquiring the lock as a reader requires the same bus transaction—a read-to-own—as acquiring a mutex, and contention on that line can hurt every bit as much.

           直接翻译过来:小心读写锁。一个非常容易犯的初级错误就是当看到一个数据写的次数远多于读的次数的时候为了并发的读就使用读写锁。使用的时候看起来合理但是必须在一种特殊的情况下,锁的时间很长,如果每次锁的时间很段还是用读写锁,那么使用读写锁的效率还不如使用一般的锁。为什么呢?因为读写锁自己有一个状态更新的时候必须是原子操作,而在目前缺少精妙而且节省空间的同步原语的情况下,读写锁用一个字节的内存存储读的次数。因为这个数字必须被原子的更新,获取读锁的时候与获取mutex有着同样的总线事务,而且竞争造成的开销几乎一样。

           通过上面的解释可以发现,使用读写锁,如果与mutex获取锁次数一样的话,从机器性能上的开销基本是一样的,而且还可能比mutex稍差一点,可为什么还是用读写锁呢?在一些特殊情况下,获取锁的时间比较长,这个时候用读写所就可以让多个线程并发的去读,从而提高处理效率,但是这些特殊的任务还要满足另外一个特点“读的次数远多于写”,因为如果读写次数差不多的话,一次读一次写,刚读一次,就要写,阻塞了其他的读,这个时候并没有“读并发”而且从锁效率上看,不如一般的mutex,也就是说如果不满足“读的次数远多于写”那么就不能发挥的写锁的特性,而且效率会比一般的锁低。如果锁持有的时间很快,读的时候只读取几十个字节的内存,还用读写锁,那么很可能会造成系统性能的下降,既然读取已经很快还有必要并发的读么?如果只是读取十几个字节还要排队竞争去读,读一次还需要个1秒钟,那么cpu一直在忙于读取(内存操作很快)和锁竞争,那么使用读锁前面的系统开销一点没有减少,反而可能因为使用读写锁会增加系统开销。

    There are still many situations where long hold times (e.g., performing I/O under a lock as reader) more than pay for any memory contention, but one should be sure to gather data to make sure that it is having the desired effect on scalability. Even in those situations where a readers/writer lock is appropriate, an additional note of caution is warranted around blocking semantics. If, for example, the lock implementation blocks new readers when a writer is blocked (a common paradigm to avoid writer starvation), one cannot recursively acquire a lock as reader: if a writer blocks between the initial acquisition as reader and the recursive acquisition as reader, deadlock will result when the recursive acquisition is blocked. All of this is not to say that readers/writer locks shouldn’t be used—just that they shouldn’t be romanticized.

           读写锁使用能提高效率的场景与多线程类似,需要在较长时间持有锁的情况下,如果特别长,干脆开个线程,而且读写锁还要求,读次数远多于写。对于第二段说明的死锁的情况,“If, for example, the lock implementation blocks new readers when a writer is blocked (a common paradigm to avoid writer starvation), one cannot recursively acquire a lock as reader: if a writer blocks between the initial acquisition as reader and the recursive acquisition as reader, deadlock will result when the recursive acquisition is blocked.”验证代码如下,就是读锁在递归调用(重入的时候),如果有写锁,那么会发生死锁,发生死锁的一个前提是写锁blocked的时候,不在允许新的读锁acquire(否则可能写所一直无法写,造成写饥饿的状态),这个也是读写锁使用时候需要特别注意的地方(避免读锁重入!)。假设一个调用的过程为

    (###ACE's rwlock 实现有问题###)


    rwlock.acquire_read();
    rwlock.acquire_read();
    rwlock.release();
    rwlock.release();


    如果在第一次rwlock.acquire_read()后另外一个线程有rwlock.acquire_write()操作,而发生这个操作后“blocks new readers”,也就是第二次的rwlock.acquire_read()被阻塞住了,这个时候就发生了死锁,acquire_write()会让第二个rwlock.acquire_read();阻塞住,而第一个rwlock.acquire_read();也无法release,这个时候就会有死锁。验证的代码如下(使用ACE):

      1. class CRWLockTest : public ACE_Task_Base  
      2. {  
      3. public:  
      4.  virtual int svc (void)  
      5.  {  
      6.   cout<<"svc() - Start"<<endl;  
      7.   Get();  
      8.   return 0;  
      9.  }  
      10.   
      11.  int Get()  
      12.  {  
      13.   cout<<"Get() - Begin acquire read..."<<endl;  
      14.   m_lockRW.acquire_read();  
      15.   cout<<"Get() - Acquire success."<<endl;  
      16.   
      17.   cout<<"Read() - Begin sleep..."<<endl;  
      18.   /// 等待读锁读  
      19.   ACE_OS::sleep(5);  
      20.   cout<<"Read() - End sleep..."<<endl;  
      21.   
      22.   /// 递归调用读锁  
      23.   Read();  
      24.   
      25.   cout<<"Get() - Begin release..."<<endl;  
      26.   m_lockRW.release();  
      27.   cout<<"Get() - Release success."<<endl;  
      28.   return 0;  
      29.  }  
      30.   
      31.  int Read()  
      32.  {  
      33.   cout<<"Read() - Begin acquire read..."<<endl;  
      34.   m_lockRW.acquire_read();  
      35.   cout<<"Read() - Acquire success."<<endl;  
      36.   
      37.     
      38.   cout<<"Read() - Begin release..."<<endl;  
      39.   m_lockRW.release();  
      40.   cout<<"Read() - Release success."<<endl;  
      41.   return 0;  
      42.  }  
      43.   
      44.  int Write()  
      45.  {  
      46.   cout<<"Write() - Begin acquire write..."<<endl;  
      47.   m_lockRW.acquire_write();  
      48.   cout<<"Write() - Acquire success."<<endl;  
      49.     
      50.   cout<<"Write() - Begin release..."<<endl;  
      51.   m_lockRW.release();  
      52.   cout<<"Write() - Release success."<<endl;  
      53.   return 0;  
      54.  }  
      55.   
      56. private:  
      57.  ACE_RW_Thread_Mutex m_lockRW;  
      58. };  
      59.   
      60. int ACE_TMAIN (int argc, ACE_TCHAR *agrv[])  
      61. {  
      62.  CRWLockTest rwLockTest;  
      63.   
      64.  /// 开线程,调用svc方法,读锁  
      65.  rwLockTest.activate();  
      66.   
      67.  /// Sleep为了先让rwLockTest线程中Get()方法先使用读锁  
      68.  ACE_OS::sleep(2);  
      69.   
      70.  /// 发生死锁  
      71.  rwLockTest.Write();  
      72.   
      73.  ACE_OS::sleep(1000);  
      74.  return 0;  
      75. }  
  • 相关阅读:
    es6箭头函数 this 指向问题
    vue脚手架搭建移动端项目--flexible.js
    vue通过自定义指令 v-py 将名字转拼音
    ES6相关
    闭包
    apply和call与this
    RPC
    Linux根目录下各目录文件类型及各项缩写全称
    网站分析相关
    [转]作为首席架构师,我是如何选择并落地架构方案的?
  • 原文地址:https://www.cnblogs.com/kex1n/p/7977967.html
Copyright © 2011-2022 走看看