书中提到假设有下面代码:
MutexLock mutex;
std::vector<Foo> foos;
void post(const Foo &f) {
MutexLockGuard lock(mutex);
foos.push_back(f);
}
void traverse() {
MutexLockGuard lock(mutex);
for(std::vector<Foo>::const_iterator it = foos.begin(); it != foos.end(); ++ it) {
it -> doit();
}
}
如果 doit 中调用了 post 函数。
- 如果锁是非递归的,那么将会发生死锁。
- 如果锁是递归的,那么 push_back 可能导致 vector 扩容,而造成迭代器失效
一种解决方式是,copy-on-write
typedef std::vector<Foo> FooList;
typedef boost::shared_ptr<FooList> FooListPtr;
MutexLock mutex;
FooListPtr g_foos;
// 对于 g_foos 来说,此函数是读端
void traverse() {
FooListPtr foos;
{
MutexLockGuard lock(mutex); // 保护 shared_ptr
foos = g_foos;
}
for(std::vector<Foo>::const_iterator it = foos -> begin(); it != foos -> end(); ++ it) {
it -> doit();
}
}
// 对于 g_foos,此函数是写端
void post(const Foo &f) {
printf("post\n");
MutexLockGuard lock(mutex);
if(!g_foos.unique()) {
g_foos.reset(new FooList(*g_foos));
printf("copy the whole list\n"); // 练习:将这句话移除临界区
}
assert(g_foos.unique());
g_foos -> push_back(f);
}
即使 doit() 中调用了 post,仍不会发送死锁,这是很显然的,在 traverse 临界区之后,mutex 就已经是未加锁状态。同时,也不会出现数据错误,因为采用了 COW。
错误1:直接修改 g_foos 所指的 FooList
void post(const Foo& f) {
MutexLockGuard lock(mutex);
g_foos -> push_back(f);
}
这个配合着 shared_ptr 的 traverse(),显然是错误的。
错误2:试图缩小临界区,把 copying 移除临界区
void post(const Foo &f) {
FooListPtr newFoos(new FooList(*g_foos));
newFoos -> push_back(f);
MutexLockGuard lock(mutex);
g_foos = newFoos;
}
将 copying 放在临界区外,没有保证数据的一致性
错误3:把临界区拆成两个小的,把 copying 放到临界区外
void post(const Foo &f) {
FooListPtr oldFoos;
{
MutexLockGuard lock(mutex);
oldFoos = g_foos;
}
FooListPtr newFoos(new FooList(*oldFoos));
newFoos -> push_back(f);
MutexLockGuard lock(mutex);
g_foos = newFoos;
}
这里的问题其实和错误2问题一样,只是看起来先复制了以下 g_foos,但是其对象的数据一致性仍没有保证
mutex 替换 rwlock
书中提到,rwlock 其开销大于 mutex,因为会需要维护读锁和写锁,同时还需要维护读锁的数量,并且写锁优先于读锁。所以一些情况下,rwlock 并不一定优于 mutex,即使是读的频率大于写的频率。
其替换方式仍旧是 mutex+cow.
当然,哪一个更适用并不是说的,需要进行实际测试。(只是提供多一种方案罢了)