使用线程时候,稍不注意就会发生死锁。A线程独占A资源,B线程独占B资源,这个时候A线程希望占用B资源,而B资源在没有释放所有权的时候又去尝试占用A资源,这个时候AB线程就永远处于相互等待状态,也就是死锁了。
1、单个线程死锁
单线程死锁情况比较简单,就是一个线程申请了锁,还没释放又去申请一次。代码如下:
#include <thread>
#include <mutex>
/** 互斥量
*/
static std::mutex s_mutex;
int main()
{
s_mutex.lock();
s_mutex.lock();
getchar();
return 1;
}
主线程中连续lock两次,就会发生死锁,但是这种简单的情况C11有作说明:
If the mutex is currently locked by the same thread calling this function, it produces a deadlock (with undefined behavior). See recursive_mutex for a mutex type that allows multiple locks from the same thread.
连续两次lock官方文档说是可能会发生死锁,它是未定义行为。事实上在windows上的实现是抛出异常,但是linux上好像是死锁。
这种情况比较简单也比较容易避免,需要注意的是在函数中嵌套调用,比如:
#include <thread>
#include <mutex>
/** 互斥量
*/
static std::mutex s_mutex;
void Fun1()
{
s_mutex.lock();
// do something
s_mutex.unlock();
}
void Fun()
{
s_mutex.lock();
Fun1();
s_mutex.unlock();
}
int main()
{
Fun();
getchar();
return 1;
}
Fun() 中就发生了连续调用的情况。当代码膨胀的时候,就算是一个人写代码也难以避免这种情况,更不用说是和同事合作了。但幸运的是这种问题很容已解决,只要将std::mutex 换成std::recursive_mutex即可。
还有一种比较奇怪的地方,一个来自leetcode的题:

有这么一个答案:
// 执行用时为 8 ms 的范例
#include <mutex>
class FooBar {
private:
int n;
std::mutex lock_bar_;
std::mutex lock_foo_;
public:
FooBar(int n) {
this->n = n;
lock_bar_.lock();
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
lock_foo_.lock();
// printFoo() outputs "foo". Do not change or remove this line.
printFoo();
lock_bar_.unlock();
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
lock_bar_.lock();
// printBar() outputs "bar". Do not change or remove this line.
printBar();
lock_foo_.unlock();
}
}
};
lock_foo_ 在函数foo线程中循环加锁来达到等待目的(wait),在bar线程中解锁使等待状态结束(notify);有两个奇怪的地方
1、一个线程连续加锁两次是个未定义行为,上面说过了。
2、一个线程加锁,可以在另外一个线程中解锁吗?换句话说,当一个线程没有拥有锁的时候(没有调用过lock)可以调用unlock吗?C11的文档同样指出这也是一个未定义行为:If the mutex is not currently locked by the calling thread, it causes undefined behavior.
所以上面的解法在一个环境中正确,换到另外一个环境中就不正确了,请避免这种写法。
2、多个线程中死锁
多个线程死锁一般都是发生了相互等待的行为,比如文章开头的AB资源等待,代码如下:
#include <thread>
#include <mutex>
/** 互斥量
*/
static std::mutex s_mutexA;
static std::mutex s_mutexB;
void FunA()
{
s_mutexA.lock();
// 为了让B线程能够先获得s_mutexB
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
s_mutexB.lock();
// 运行不到这里
s_mutexB.unlock();
s_mutexA.unlock();
}
void FunB()
{
s_mutexB.lock();
// 为了让A线程能够先获得s_mutexA
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
s_mutexA.lock();
// 运行不到这里
s_mutexA.unlock();
s_mutexB.unlock();
}
int main()
{
std::thread A(FunA);
std::thread B(FunB);
getchar();
return 1;
}
其实这种情况还是比较容易发现并且避免的,只要在另外一个锁lock之前,将前一个释放就好了(如果因为各种原因,第一个锁保护的资源一定要等到第二个锁lock后才能释放的话,我没有发现好的方法,遇到了就想办法避免它)。前文讲到,可以使用std::lock_guard来避免忘记unlock这个时候就需要将锁放到一对花括号中,来保证在第二个锁lock前释放第一个:
void FunA()
{
{
std::lock_guard<std::mutex> guardA(s_mutexA);
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
{
std::lock_guard<std::mutex> guardB(s_mutexB);
}
}
void FunB()
{
{
std::lock_guard<std::mutex> guardB(s_mutexB);
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
{
std::lock_guard<std::mutex> guardA(s_mutexA);
}
}
去掉那些花括号就会死锁,用花括号来管理这个资源自动释放(RAII)是很常见的写法。
对于两个线程循环lock会死锁:
A->B->A
扩展到多个线程同样会死锁:
A->B->C->D->.....->A
但是对于这种情况如何避免我没有找到好的方法,只能凭借经验了~~~
3、请注意阻塞函数的回调
windwows 的窗口消息循环中有一个著名的函数:SendMessage,他是一个阻塞函数,必须等到他的消息被处理完成后才会返回:
SendMessge -等待> CallBack
这个时候如果有lock的话很容易死锁:
std::mutex s_mu;
// 一个线程
void ThreadFunA()
{
s_mu.lock();
//
::SendMessage(...)
s_mu.unlock();
}
// 消息循环回调,一般在主线程
void MessageCallBack()
{
// 这里会死锁
s_mu.lock();
s_mu.unlock();
}
有很多这样的跨线程回调,如果遇到,请留心。
总结
容易发生死锁的地方:
1、忘记unlock:使用lock_guard管理mutex
2、同一个线程多次lock:避免这样写,这是一个UB行为,同样的避免在一个线程没有获得锁的情况下unlock这也是一个UB
3、多线程循环调用:没有好的方法,可以通过设计在某个地方打破循环,比如尽量缩短锁的占用时间
4、跨线程框架中提供了阻塞回调,遇到这种情况请小心使用锁
下一篇将介绍任务队列,一种供上层调用绕开使用锁的简单方法。
本文深入探讨了死锁现象,包括单线程和多线程死锁的原因与实例。通过代码示例,详细分析了死锁的发生机制,并提出了有效的预防措施,如使用std::lock_guard和设计合理的锁顺序。

2258

被折叠的 条评论
为什么被折叠?



