上周做性能调优的时候,发现一个测并发读写的场景数据很奇怪。
场景是测一个写线程加不同数量的读线程时的读写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
...
总之写线程连续抢到若干次锁后,可怜的读线程才抢到一次锁。
很奇怪的现象。
我之前对读写锁抢锁流程的理解:
- 如果当前没有线程持有锁,那么第一个去抢锁的活动线程会拿到锁;
- 如果当前持有者释放锁,那么所有排队的线程会进行抢锁;
- 排队的线程中有等写锁的线程时,申请读锁会阻塞(写锁优先)。
现在看来这个理解是有问题的,无法解释这一现象。
和同事交流了一下,读写锁抢锁流程可能是这样的:
- 如果有线程申请锁阻塞,会首先调用SpinLock一会,之后如果还是没抢到锁,那么内核将其设置为睡眠状态,并加入等待队列;
- 当前持有线程释放锁后,内核将所有等待队列中的睡眠线程唤醒,加入调度队列;
- 进入调度队列的竞争线程在被调度运行后,开始抢锁。
从这个流程来看,我遇到的这种情况可以这么解释:
- 读线程首先运行,抢到锁;
- 因为是写优先,在读线程结束后锁肯定会让给写线程;
- 写线程释放锁后,读线程被唤醒,此时还处于等待状态,未运行,不能抢锁;
- 写线程没有睡眠,重新抢锁,此时没有写优先的影响,成功抢到锁;
- 读线程开始运行,抢锁失败,重新睡眠。
在读线程增多以后,写线程释放锁后就不一定能抢到锁了,因此会有一定的时间在睡眠,这样进一步增大了读线程抢到锁的概率,因此就观察到读的QPS猛涨的情况。
上面的猜测还未得到验证,有空还是得看看pthread_read_write_lock的实现啊。