《C++ Concurrency in Action》笔记8 死锁(1)

就像之前关于top和pop的讨论,他的问题本质上来说是因为lock了太小的单元,保护并不能覆盖几个操作的组合。但是,另一方面,如果lock了太大的范围,同样也能导致问题。极端的情况就是一个全局mutex对象保护了所有的共享数据。在一个存在着大量共享数据的系统中,这将抵消并发带来的任何效益增值,因为它强迫在同一时刻只能运行一个线程,即使这些线程访问的是不同的共享数据。Linux内核的第一个版本就是被设计成使用一个全局内核锁。尽管他能工作,但是这意味着一个双核系统的性能明显低于两个单核系统。后来的Linux版本采用了更加细粒度的锁策略,解决了这个问题。

使用细粒度锁方案的一个问题就是,为了在一个操作中保护所有数据,你需要更多的mutex。如前所说,有时候需要增加锁粒度,以至只需要一个mutex去执行锁定操作。但有时候这是不可取的,例如多个mutex保护了一个类的多个实例。

In this case, locking at the next level up would mean either leaving the locking to the user or having a single mutex that protected all instances of that class, neither of which is particularly desirable.

在这种情况下,进入下一层锁就意味着要么将锁丢给用户去执行,要么使用一个单独的mutex去保护这个类的所有实例,但两者都不令人满意。(不知道翻译的是否准确)

如果你最终不得不在一个操作中使用多个mutex,那么还有另外一个隐藏的风险:死锁。这种情况几乎完全与race condition相反,多个线程都想争得控制权,但却都在等待对方释放控制权,结果只能无限等下去。

一种常见的避免死锁的方式是,总是以同样的顺序锁定2个mutex。但有时不太容易,例如多个mutex负责保护一个类的多个实例。想象一个例子:一个操作用于交换同一个类的两个实例,如果选择固定顺序(例如,先是锁定与第一个参数对应的mutex,然后再锁定与第二个参数对应的mutex),那将适得其反:如果两个线程同时针对相同的一对实例进行交换操作,只是参数顺序不同,那么将产生死锁。

幸运的是,C++标准库提供了一个函数来解决这种问题:std::lock(),可以一次性锁定2个或更多的mutex,而且不会导致死锁。下面的示例演示如何使用这个函数:

class some_big_object
{

};
class X
{
private:
	some_big_object some_detail;
	std::mutex m;
public:
	X(some_big_object const& sd) :some_detail(sd) {}
	friend void swap(X& lhs, X& rhs)
	{
		if (&lhs == &rhs)
			return;
		std::lock(lhs.m, rhs.m);
		std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
		std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
		swap(lhs.some_detail, rhs.some_detail);
	}
};

首先检查两个参数是否不同,因为试图锁定一个已经被自己锁定的mutex是未定义行为( std::recursive_mutex可以被同一个线程多次锁定)。然后使用std::lock()函数锁定两个mutex,再构造2个std::lock_guard对象,std::adopt_lock参数用于通知std::lock_guard对象:为其提供的mutex已经被锁定,在构造时不要再次锁定mutex参数。然后进行具体的数据交换。当函数执行完毕后,2个std::lock_guard对象的析构函数负责解锁其对应的mutex对象,这一点我们通过查看std::lock_guard的源码(vs2013)可以看得十分清楚:

template<class... _Mutexes>
	class lock_guard
	{	// class with destructor that unlocks mutexes
public:
	explicit lock_guard(_Mutexes&... _Mtxes)
		: _MyMutexes(_Mtxes...)
		{	// construct and lock
		_STD lock(_Mtxes...);
		}

	lock_guard(_Mutexes&... _Mtxes, adopt_lock_t)
		: _MyMutexes(_Mtxes...)
		{	// construct but don't lock
		}

	~lock_guard() _NOEXCEPT
		{	// unlock all
		_For_each_tuple_element(
			_MyMutexes,
			[](auto& _Mutex) _NOEXCEPT { _Mutex.unlock(); });
		}

	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;
private:
	tuple<_Mutexes&...> _MyMutexes;
	};

此处仅将lock_guard类模板的部分定义贴了出来,还有两个模板特化代码没有贴出来分别是只有一个mutex参数和没有参数的定义。如果没有参数则lock_guard什么也不做。

具有std::adopt_lock类型参数的构造函数,其实这个参数仅仅起到一个通知的作用,此时构造函数不去对mutex执行lock操作。注意析构函数,无论如何都执行mutex的unlock操作。通过分析std::lock_guard的源码,也会知道,std::lock()函数仅仅负责锁定其参数对应的mutex,别的什么也不做。解锁是由std::lock_guard来做的。

注意多参数模板定义的构造函数,在函数体内自动调用了std::lock()函数。因此上面的swap()函数可以简化成如下版本:

void swap1(X& lhs, X& rhs)
{
	if (&lhs == &rhs)
		return;
	lock_guard<mutex,mutex> lg(lhs.m, rhs.m);
	swap(lhs.some_detail, rhs.some_detail);
}

还要注意的是,std::lock()函数在试图锁定一个mutex时可能引发异常,例如,当成功锁定一个mutex后在试图锁定第二个mutex时即使产生来了异常一会保证释放第一个mutex。它遵循一个原则:要么都锁定要么都不锁定。

虽然,std::lock()函数可以帮你同时锁定多个mutex,但是它无法解决这些mutex同时被其他线程分别单独锁定而带来的问题,此时只能靠一个程序员的经验了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值