C++死锁问题

本文由一个简单的例子详细探讨下死锁是怎么发生的,以及如何避免死锁问题的发生。

死锁是怎么发生的?

死锁比较官方的说法是:是指两个或多个线程在执行过程中,由于争夺资源而造成的一种互相等待的现象,从而导致程序无法继续执行。

上述说可能比较抽象,个人理解当线程1在持有互斥锁A的同时请求互斥锁B,同时线程2持有互斥锁B的同时在请求互斥锁A。即线程1在等待线程2执行完成后将锁B释放,但是线程2在等待线程1执行完后将锁A释放,由此发生了互相等待的情况,即产生了死锁。

代码示例A:

假如我们有2个变量,想使用两个互斥锁分别同步这2个变量的访问。在两个线程中,分别对这两个变量都进行了读或写。如下面代码发生了死锁

#include <iostream>
#include <algorithm>
#include <thread>
#include <mutex>
using namespace  std;

std::mutex mtx_a;
std::mutex mtx_b;

int a = 0, b = 0;

// 写入a
void set_a(int tmp)
{
    cout << "set_a...1" << endl; //a资源锁定前
    std::unique_lock<mutex> lock_a(mtx_a);
    cout << "set_a...2" << endl; //a资源已锁定
    a = tmp;
    this_thread::sleep_for(chrono::seconds(1)); //为了增加触发死锁几率,增加等待,模拟工程中的函数处理需要时间
    cout << "set_a...3" << endl; //a资源即将解锁
}

// 写入b
void set_b(int tmp)
{
    cout << "set_b...1" << endl; //b资源锁定前
    std::unique_lock<mutex> lock_b(mtx_b);
    cout << "set_b...1" << endl; //b资源锁定前
    b = tmp;
    this_thread::sleep_for(chrono::seconds(1)); //为了增加触发死锁几率,增加等待,模拟工程中的函数处理需要时间
    cout << "set_b...1" << endl; //b资源锁定前
}

void func_1()
{
    cout << "func_1...1" << endl; //a资源锁定前
    std::unique_lock<mutex> lock_a(mtx_a); // func_1持有mtx_a
    cout << "a=" << a << endl; //读取a
    cout << "func_1...2" << endl; //请求b资源前
    set_b(a+1); //调用的函数会请求mtx_b
    cout << "func_1...3" << endl;
}

void func_2()
{
    cout << "func_2...1" << endl; //b资源锁定前
    std::unique_lock<mutex> lock_b(mtx_b); // func_2持有mtx_b
    cout << "b=" << b << endl; //读取b
    cout << "func_2...2" << endl; //请求a资源前
    set_a(b+1); //调用的函数会请求mtx_a
    cout << "func_2...3" << endl;
}

int main(){
    auto th_1 = thread(func_1);
    auto th_2 = thread(func_2);

    th_1.join();
    th_2.join();

    cout << "main finished" << endl;

    return 0;
}

输出:

从结果可见,我们无法等待这段程序自动执行完成了,因为发生了死锁。

我们从输出这些打印信息,分析下都发生了什么:

首先func_1、func_2两个线程并发执行,所以在第一行中func_1...1、func_2...1分别输出,甚至由于输出不是原子操作,回车符的输出都被“打断”了;

然后线程1获取锁a,线程2获取锁b后,两个线程分别正常输出a和b的值;

同样的,两个线程在执行完fun_1(2)...2的输出后,分别进入set_b,set_a的函数调用;

进入set函数调用后,两个线程要分别获取锁b和锁a,但是由于锁a被线程1占用中,线程2则在等待,无法继续执行;同样的,锁b被线程2占用中,线程1也无法继续执行。两个线程都无法继续执行。

死锁怎么预防

发生死锁的根本原因还是对锁的使用不当。所以为了避免发生死锁,我们还是要从程序设计上入手,预防死锁的发生。

下面是一些有效的预防死锁方法:

1、资源排序法

在合适的场景下,对资源的访问进行一个全局的排序,各个线程按照同一顺序对资源进行请求。

在上面的例子中,假如我们统一规定,对a,b两个资源的访问顺序为a,b。对fun_2的资源访问调换顺序,如下,则就不会发生死锁。

但是要注意的是,这样可能改变了a或b输出的值,因为这两个值的设置互相依赖,所以我们要结合需求,解决死锁问题。

void func_2()
{
    cout << "func_2...1" << endl;
    set_a(b+1);
    cout << "func_2...2" << endl;
    std::unique_lock<mutex> lock_b(mtx_b);
    cout << "b=" << b << endl;
    cout << "func_2...3" << endl;
}

输出:

程序正常执行完成

2、使用定时锁

这里介绍定时锁std::timed_mutex,它听过try_lock_for函数可以设置尝试锁定时间。timed_mutex也可以结合unique_ptr管理使用。

如上述代码A,我们将set_a的函数改成如下形式,或者set_a、set_b都进行修改都可以解决死锁问题:

std::timed_mutex mtx_a; //修改为定时锁

void set_a(int tmp)
{
    int max_retry = 3; //设置最大超时重试次数
    while(max_retry--)
    {
        std::unique_lock<timed_mutex> lock_a(mtx_a, std::defer_lock); //设置初始为不锁定状态
        if(lock_a.try_lock_for(chrono::seconds(2))) //尝试锁定锁a,超时时间2s
        {
            a = tmp; //设置a的值
            this_thread::sleep_for(chrono::seconds(1));
            return; //执行成功,返回调用
        }
        else //尝试锁定操作超时,则循环尝试再次锁定
        {
            this_thread::sleep_for(chrono::seconds(1)); //设置1s等待时间,避免忙等待
        }
    }
}

输出可见,程序成功退出死锁:

3、避免嵌套锁

什么是嵌套锁?其实在示例代码A中,我们就是使用了嵌套锁。即在锁定a的锁定中去尝试获取(锁定)锁b。

嵌套锁是一个比较常见的发生死锁的原因。例A中的代码比较简单很容易发现,但是在日常工作中,我们写的代码通常具有复杂的调用关系,如果代码习惯不好很容易造成嵌套锁。

避免嵌套锁,我们要注意对每个锁的作用有个清晰的认知,合理使用锁,及时释放锁。

比如在例A中,我们尝试用锁a同步对变量a的访问;锁b同步对变量b的锁定。但是在例A的代码设计中,我们在func_1中使用锁a管理了对a的读取,同时也锁住了对set_b(设置b)的调用。如果我们并不需要这种设计,我们可以在访问变量a结束后,及时将锁a释放,这样避免了锁的嵌套。

修改func_1、func_2如下

只是增加了 lock.unlock(); 一行代码

void func_1()
{
    cout << "func_1...1" << endl;
    std::unique_lock<mutex> lock_a(mtx_a);
    cout << "a=" << a << endl;
    lock_a.unlock(); //及时对锁a进行释放
    cout << "func_1...2" << endl;
    set_b(a+1);
    cout << "func_1...3" << endl;
}

void func_2()
{
    cout << "func_2...1" << endl;
    std::unique_lock<mutex> lock_b(mtx_b);
    cout << "b=" << b << endl;
    lock_b.unlock(); //及时对锁b进行释放
    cout << "func_2...2" << endl;
    set_a(b+1);
    cout << "func_2...3" << endl;
}

4、避免自引用

什么是自引用?

如果一个线程在持有锁的情况下调用某一个函数,同时该函数又尝试获取同一个锁,即发生了自引用。

例如如下代码,就是自引用的

void func_3()
{
    std::unique_lock<mutex> lock_a(mtx_a);
    cout << a << endl;
    set_a(6); // set_a中也尝试获取了mtx_a,发生自引用
}

5、避免长时间持有锁,在锁的范围内只执行必要的操作

如标题,这是一个程序设计的原则,不需要过多解释,相信大家都能够理解。

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++死锁是指在多线程编程中,两个或多个线程互相等待对方释放资源而无法继续执行的情况。这种情况下,程序会陷入无限等待的状态,无法正常运行。 死锁通常发生在多个线程同时访问共享资源时,每个线程都持有一个资源,并且试图获取其他线程持有的资源。当所有线程都无法获取到所需的资源时,就会发生死锁。 为了避免死锁的发生,可以采取以下几种方法: 1. 避免使用多个锁:尽量减少使用多个锁来控制资源的访问,可以使用更细粒度的锁或者使用无锁数据结构来避免死锁的发生。 2. 使用加锁顺序:对于多个资源,确保所有线程以相同的顺序获取锁,这样可以避免死锁的发生。 3. 使用超时机制:在获取锁的过程中设置超时机制,如果超过一定时间还未获取到所需的资源,就主动释放已经获取的资源,避免陷入死锁死锁检测工具是用来检测程序中是否存在潜在的死锁问题的工具。常见的死锁检测工具有: 1. Valgrind:一个开源的内存调试和性能分析工具,其中包含了Helgrind工具,可以检测多线程程序中的死锁问题。 2. ThreadSanitizer:一个用于检测并发错误的工具,可以检测死锁、数据竞争等问题。 3. Visual Studio的并发视图器:在Visual Studio中提供了一个并发视图器,可以帮助开发者分析多线程程序中的死锁问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值