读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
读写锁实际是一种特殊的
自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在
多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。
在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
一次只有一个
线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性,
当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.
当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁.
通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁请求长期阻塞.
读写锁适合于对
数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁.
所谓「读写锁」,就是同时可以被多个读者拥有,但是只能被一个写者拥有的锁。而所谓「多个读者、单个写者」,并非指程序中只有一个写者(线程),而是说不能有多个写者同时去写。
下面看一个计数器的例子。
class Counter {
public:
Counter() : value_(0) {
}
// Multiple threads/readers can read the counter's value at the same time.
size_t Get() const {
boost::shared_lock<boost::shared_mutex> lock(mutex_);
return value_;
}
// Only one thread/writer can increment/write the counter's value.
void Increase() {
// You can also use lock_guard here.
boost::unique_lock<boost::shared_mutex> lock(mutex_);
value_++;
}
// Only one thread/writer can reset/write the counter's value.
void Reset() {
boost::unique_lock<boost::shared_mutex> lock(mutex_);
value_ = 0;
}
private:
mutable boost::shared_mutex mutex_;
size_t value_;
};
shared_mutex
比一般的 mutex
多了函数 lock_shared() / unlock_shared()
,允许多个(读者)线程同时加锁、解锁,而 shared_lock
则相当于共享版的 lock_guard
。
对 shared_mutex
使用 lock_guard
或 unique_lock
就达到了写者独占的目的。
测试代码:
boost::mutex g_io_mutex;
void Worker(Counter& counter) {
for (int i = 0; i < 3; ++i) {
counter.Increase();
size_t value = counter.Get();
boost::lock_guard<boost::mutex> lock(g_io_mutex);
std::cout << boost::this_thread::get_id() << ' ' << value << std::endl;
}
}
int main() {
Counter counter;
boost::thread_group threads;
threads.create_thread(boost::bind(Worker, boost::ref(counter)));
threads.create_thread(boost::bind(Worker, boost::ref(counter)));
threads.join_all();
return 0;
}
输出(仍然是随机性的):
当然,对于计数器来说,原子类型 boost::atomic<>
也许是更好的选择。
假如一个线程,先作为读者用 shared_lock
加锁,读完后突然又想变成写者,该怎么办?
方法一:先解读者锁,再加写者锁。这种做法的问题是,一解一加之间,其他写者说不定已经介入并修改了数据,那么当前线程作为读者时所持有的状态(比如指针、迭代器)也就不再有效。
方法二:用 upgrade_lock
,它可以当做 shared_lock
用,但是必要时可以直接从读者「升级」为写者。
{
// Acquire shared ownership to read.
boost::upgrade_lock<boost::shared_mutex> upgrade_lock(shared_mutex_);
// Read...
// Upgrade to exclusive ownership to write.
boost::upgrade_to_unique_lock<boost::shared_mutex> unique_lock(upgrade_lock);
// Write...
}
可惜的是,我没能给 upgrade_lock
找到一个颇具实际意义的例子。