借 shared_ptr 实现 copy-on-write(避免死锁,mutex替换rwlock)

书中提到假设有下面代码:

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.
 
当然,哪一个更适用并不是说的,需要进行实际测试。(只是提供多一种方案罢了)

你可能感兴趣的:(Linux多线程服务端编程)