您的位置:首页 > 其它

一种读写公平的读写锁

2016-05-24 15:26 288 查看

一、概述

所谓读写锁,就是同一时间允许有多个线程读,但是一旦写者要写,那么在写的过程中别的线程就不允许读写。

但是当写者在申请写锁时,可能有读者正在读,这时不同的读写锁有不同的表现,有下面几种。

读者优先:只要有读者要读,写者就不能写,如果前面的读者还没走就来了新的读者,那么新的读者又会读,所以写者可能饿死。

写者优先:写者一到就开始写,即使这时还有读者没读完。当写者写完后,以前的读者需要重新读。

读写公平:之前的读者读完后,写者开始写;写者写完后,后面的读者才开始读。

这篇博客的主题是读写公平,另外两种以后的博客里再写。

为了实现读写公平,要点在于记录有没有写者在等待;如果有写者在等待,那么新的读者也必须等待;否则读者就可以直接读。

二、一种会阻塞线程的写法

class RWLock {
public:
RWLock() : mCount() { }
void readLock();
void readUnlock();
void writeLock();
void writeUnlock();
private:
mutex mMutex;
condition_variable mCV;
size_t mCount;
bool mExclusive;
};

void RWLock::readLock() {
unique_lock<mutex> lk(mMutex);
while (mExclusive && mCount > 0) {
mCV.wait(lk);  // 等待前面的写者
}
mExclusive = false;
++mCount;
}

void RWLock::readUnlock() {
assert(!mExclusive && mCount > 0);
lock_guard<mutex> lk(mMutex);
--mCount;
if (mCount == 0) {
mCV.notify_all();  // 下面有详细解释
}
}

void RWLock::writeLock() {
unique_lock<mutex> lk(mMutex);
while (mCount > 0) {
mExclusive = true;
mCV.wait(lk);  // 有线程在读或写
}
mExclusive = true;
++mCount;
}

void RWLock::writeUnlock() {
assert(mExclusive && mCount == 1);
lock_guard<mutex> lk(mMutex);
--mCount;
mCV.notify_all();  // 唤醒所有读写线程
}
这只是伪代码,我没有编译的,这里面的mutex和condition_variable分别以C++11标准库里面的为蓝本。

实现的过程中用到了条件变量,条件变量附带一个等待队列:线程在入队等待时会把锁放掉,出队苏醒时又会重新获得锁。

如果线程出队苏醒时上锁失败,则这个线程事实上又会进入互斥锁的等待队列,看具体实现的聪不聪明了,可能它会直接把线程从一个队列移到另一个队列,也可能会让线程唤醒然后又马上沉睡。反正逻辑上看都是一样。

这些伪代码中另外还有很多困难的地方,包括:

1.mExclusive这个变量可能被编译器优化到while循环外面去,我不确定如果加上volatile有没有用,感觉这样实现就是不太对。

2.仍然不是按照线程到来的顺序上锁的,把线程从条件变量的等待队列里面移到互斥锁的等待队列时会导致先来的线程反而排在后面。

3.虽然读数据的时候多个读者会一起读,但是上锁的时候多个读者还是会互相阻塞,不知道用原子操作能不能解决这个问题,看上去很麻烦的样子。

readUnlock()中的注释:网上很多地方在这里都是这么写的,但是我想了很久都觉得这里只要notify_one就够了,因为这时要么是没有线程了,要么是有写者把后面的读者都挡住了,所以条件变量的等待队列里面第一个一定是写者。

综上所述,真正实现一个这样的读写锁是很困难的,想要实现得有效率就更难了,所以我这里写的只是伪代码。

三、一种让线程自旋的写法

class RWLock {
public:
RWLock() : mCount() { }
void readLock() { while(!ReadTryLock()); }
bool readTryLock();
void readUnlock();
void writeLock() { while(!writeTryLock()); }
bool writeTryLock();
void writeUnlock();
private:
spinlock mLock;
size_t mCount;
bool mExclusive;
};

bool RWLock::readTryLock() {
lock_guard<spinlock> lk(mLock);
if (mExclusive && mCount > 0) {
return false;
} else {
mExclusive = false;
++mCount;
return true;
}
}

void RWLock::readUnlock() {
assert(!mExclusive && mCount > 0);
lock_guard<spinlock> lk(mLock);
--mCount;
}

bool RWLock::writeTryLock() {
lock_guard<spinlock> lk(mLock);
mExclusive = true;
return mCount == 0;
}

void RWLock::writeUnlock() {
assert(mExclusive && mCount == 1);
lock_guard<spinlock> lk(mLock);
--mCount;
}
这里用到spinlock(自旋锁),它在上锁失败时会有一个busy loop来不停地尝试,这意味着它一直占用cpu而不是把cpu让出去,我的上一篇博客里有spinlock的实现原理解说。

四、性能

按照我的估计,多核读写小数据时自旋会比较快,单核读写大的数据(比如磁盘文件)应该用阻塞的方法。

本来应该做一个性能测试的,以后再说吧。我现在对性能没有定量的估计。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: