读写锁的一个奇怪表现

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

场景是测一个写线程加不同数量的读线程时的读写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的实现啊。

原文地址:https://www.cnblogs.com/fuzhe1989/p/3876492.html