C++ 中死锁的产生及解决

死锁产生的必要条件:

  1. 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
  2. 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  3. 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

1. 一个给定的操作存在两个或两个以上的互斥量时,容易产生死锁

例子:


class some_big_obj
{
	// ...
};

void swap(some_big_obj& lhs, some_big_obj& rhs);

class X
{
public:
	X(some_big_obj const& sd):some_detail(sd){}
	friend void swap(X& lhs, X& rhs)
	{
		if (&lhs == &rhs)
			return;

		std::lock_guard laoc_a(lhs.m); // 1
		std::lock_guard laoc_b(rhs.m); // 2

		swap(lhs.some_detail, rhs.some_detail);
	}

private:
	some_big_obj some_detail;
	std::mutex m;
};

上面的1处和2处分别锁住了两个互斥量,假如线程A在执行1的时候,线程切换d到B,B线程锁住了rhs的锁,并开始尝试锁住lhs的锁,但是发现lhs的锁已经被锁住了(被A锁住了),所以B线程发生了阻塞。然后线程切换回A,开始去尝试锁住rhs,但是rhs此时已经被B线程锁住了,此时A线程也发生了阻塞。两个线程会分别等待对方释放另一个锁,所以就无限等待了。

解决方法:

class some_big_obj
{
	// ...
};

void swap(some_big_obj& lhs, some_big_obj& rhs);
class X
{
public:
	X(some_big_obj const& sd):some_detail(sd){}
	friend void swap(X& lhs, X& rhs)
	{
		if (&lhs == &rhs)
			return;

		std::scoped_lock lock(lhs.m, rhs.m); // 1 C++17

		// 2 C++11
		// std::lock(lhs.m, rhs.m);
		// std::lock_guard lock_a(lhs.m, std::adopt_lock);
		// std::lock_guard lock_b(rhs.m, std::adopt_lock);

		swap(lhs.some_detail, rhs.some_detail);
	}

private:
	some_big_obj some_detail;
	std::mutex m;
};

有两种方法:

  1. 直接使用C++17标准的std::scope_lock模板类,此模板类可以同时锁定多个互斥量,并且能够在析构时自动解锁互斥量;
  2. 使用C++11标准的std::lock()模板函数和std::lock_guard模板类,通过函数std::lock()锁定多个互斥量,然后通过std::lock_guard类负责互斥量的解锁。其中,std::adopt_lock表示只构造对象,但不锁定互斥量。

2. 多个线程共用一个互斥量,且线程之间相互等待

例子:

std::mutex m;

void f()
{
	// ....

	std::lock_guard lock(m); // 1 子线程锁住互斥量m

	// ...
}

int main()
{
	
	std::thread t(f);
	std::lock_guard lock(m); // 2 主线程锁住互斥量m
	// ...

	t.join(); // 3 等待子线程结束

	return 0;
}

上述过程可能导致在2处上锁,然后子线程在1处发生阻塞,最后主线程在3处一直等待子线程结束,无穷等待下去。

解决方法:

  1. 主线程对互斥量上锁要放到join()之后;
	std::thread t(f);
	
	// ...
	
	t.join();
	
	// ...
	
	std::lock_guard lock(m);
	// ...

	
  1. 主线程对互斥量上锁放到一个局部作用域内。
	std::thread t(f);
	
	{
		std::lock_guard lock(m);
		// ...
	}

	t.join();

3. 避免在持有锁时调用用户提供的代码

因为用户提供的代码中很可能也有锁,这样的话,在同一个过程可能就会访问多个锁,可能会产生第1点所描述的死锁。

4. 在一些情况下,必须使用固定顺序获取锁,否则可能产生死锁

比如多线程下对于链表的操作。假如保护链表的锁的粒度很小,每个节点拥有一个互斥量,这样能够最大化并行效率。在这种情况下,当一个线程删除链表中的一个节点时,它必须获取3个节点上的互斥量:将要删除的节点,两个邻接节点。

线程1线程2
锁住主入口的互斥量
读取头节点指针
锁住头节点互斥量
解锁主入口互斥量
锁住主入口互斥量
读取head->next指针锁住尾结点互斥量
锁住next节点互斥量读取tail->prev指针
读取next->next指针解锁尾结点互斥量
锁住A节点互斥量锁住C节点互斥量
读取A->next指针(也就是B节点)读取C->next指针(也就是B节点)
锁住B节点互斥量
阻塞,尝试锁住B节点互斥量解锁C节点互斥量
读取B->prev指针(也就是A节点)
阻塞,尝试锁住A节点互斥量
死锁死锁

由于这里获取三个锁不是同时的,是分步获取的,所以不能够使用std::scoped_lock,所以就会产生上述的死锁。
避免这种死锁的方式就是定义遍历的顺序,一个线程必须先锁住A才能获取B的锁,在锁住B之后才能获取C的锁。

5. 使用层次锁来实现固定顺序上锁

在某些情况下,设定锁的大小,使得一个线程中获取多个锁时,只能按照顺序来进行上锁,如果顺序错了,直接抛出异常,这样就能避免死锁。

层次锁的实现例子:

class hierarchical_mutex
{
private:
	std::mutex internal_mutex;
	const unsigned long hierarchy_value;
	unsigned long previous_hierarchy_value;

	// thread_local是线程局存储类型
	static thread_local unsigned long this_thread_hierarchy_value;


private:
	void check_for_hierarchy_violation()
	{
		if (this_thread_hierarchy_value <= hierarchy_value)
			throw std::logic_error("mutex hierarchy violated");
	}

	void update_hierarchy_value()
	{
		previous_hierarchy_value = this_thread_hierarchy_value;
		this_thread_hierarchy_value = hierarchy_value;
	}

public:
	explicit hierarchical_mutex(unsigned long value) :
		hierarchy_value(value),
		previous_hierarchy_value(0)
	{}

	void lock()
	{
		check_for_hierarchy_violation();
		internal_mutex.lock();
		update_hierarchy_value();
	}

	void unlock()
	{
		if (this_thread_hierarchy_value != hierarchy_value)
			throw std::logic_error("mutex hierarchy violated");
		this_thread_hierarchy_value = previous_hierarchy_value;
		internal_mutex.unlock();
	}

	bool try_lock()
	{
		check_for_hierarchy_violation();
		if (!internal_mutex.try_lock())
			return false;

		update_hierarchy_value();
		return true;
	}
};

thread_local unsigned long hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);

其中,thread_local类型用于线程局部存储(STL)。需要注意的是,如果想让自定义锁类型能够使用标准库的std::lock()std::lock_guard,则必须自己实现类成员函数lock()unlock()try_lock()

层次锁的使用例子:

hierarchical_mutex high_level_mutex(10000);
hierarchical_mutex low_level_mutex(5000);
hierarchical_mutex other_mutex(6000);

int do_low_level_stuf();
int low_level_func()
{
	std::lock_guard lk(low_level_mutex);
	return do_low_level_stuf();
}

void do_high_level_stuff(int some_param);
void high_level_func()
{
	std::lock_guard lk(high_level_mutex);
	do_high_level_stuff(low_level_func());
}

void thread_a()
{
	high_level_func();
}

void do_other_stuff();
void other_func()
{
	high_level_func();
	do_other_stuff();
}

void thread_b()
{
	std::lock_guard lk(other_mutex);
	other_func();
}

上述例子中,thread_a线程先得到标记为10000的锁,然后又得到标记为5000的锁,因此能正常执行。而thread_b先得到标记为6000的锁,然后又尝试获取标记为10000的锁,这里就会出错,因为获取锁的顺序出错了,最后会抛出异常。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值