C++ 线程 死锁

本文深入探讨了死锁现象,包括单线程和多线程死锁的原因与实例。通过代码示例,详细分析了死锁的发生机制,并提出了有效的预防措施,如使用std::lock_guard和设计合理的锁顺序。

使用线程时候,稍不注意就会发生死锁。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、跨线程框架中提供了阻塞回调,遇到这种情况请小心使用锁

下一篇将介绍任务队列,一种供上层调用绕开使用锁的简单方法。

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值