C++并发编程学习(四)——死锁及其预防

文章目录

一、死锁介绍

死锁是指两个或多个进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致所有涉及的进程都无法继续执行下去。比如有两个线程,都需要同时锁住两个互斥,才可以进行某项操作,但它们分别都只锁住了一个互斥,都等着再给另一个互斥加锁。于是,双方毫无进展,因为它们同在苦苦等待对方解锁互斥。

一个经典的比喻:哲学家就餐问题

5 位哲学家围坐圆桌,每人左右各有一根筷子,吃饭需要同时拿起左右两根筷子,如果每个人都先拿左边的筷子,再等右边的,就会全部卡住,无人能吃饭,引发死锁。

产生死锁的四个条件

必须同时满足以下四点,才会发生死锁:

条件 说明 是否可破坏
1. 互斥条件(Mutual Exclusion) 资源一次只能被一个进程使用(如打印机、mutex) ❌ 通常不可破坏(资源本质决定)
2. 占有并等待(Hold and Wait) 进程已持有至少一个资源,同时等待其他被占用的资源 ✅ 可破坏
3. 非抢占条件(No Preemption) 已分配的资源不能被强制收回,只能由持有者主动释放 ✅ 可破坏
4. 循环等待(Circular Wait) 存在一个进程等待环:P₁→P₂→...→Pₙ→P₁ ✅ 可破坏

c++代码示例:

cpp 复制代码
#include <thread>
#include <mutex>

std::mutex mtx1, mtx2;

void thread1() {
    std::lock_guard<std::mutex> lock1(mtx1); // 先锁 mtx1
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lock2(mtx2); // 再锁 mtx2
}

void thread2() {
    std::lock_guard<std::mutex> lock2(mtx2); // 先锁 mtx2
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lock1(mtx1); // 再锁 mtx1
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);
    t1.join();
    t2.join(); //  程序可能永远卡住!
}

再比如,假设有两个账户 A 和 B,两个线程同时进行转账:

  • 线程1:从 A 转账到 B
  • 线程2:从 B 转账到 A
  • 每个账户有一个自己的 mutex 保护余额。

如果各自先锁"源账户",再锁"目标账户",就可能死锁:

cpp 复制代码
// 危险代码:可能导致死锁
class Account {
public:
    int balance;
    mutable std::mutex m;
    Account(int b) : balance(b) {}
};

void transfer(Account& from, Account& to, int amount) {
    std::lock_guard<std::mutex> lock1(from.m);   // 先锁 from
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 模拟延迟
    std::lock_guard<std::mutex> lock2(to.m);     // 再锁 to

    from.balance -= amount;
    to.balance += amount;
}

二、死锁防治方法

方法1:统一加锁顺序

上述转账例子,使用 std::lock() + std::adopt_lock来原子地锁定多个互斥量),从而彻底避免死锁

cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

class Account {
public:
    mutable std::mutex m;  // 每个账户一个 mutex
    int balance;

    Account(int initial_balance) : balance(initial_balance) {}
};

void transfer(Account& from, Account& to, int amount) {
    // 关键:使用 std::lock 原子地锁定多个 mutex
    std::lock(from.m, to.m);  // 无死锁风险!

    // 使用 adopt_lock 告诉 lock_guard:锁已经 acquired
    std::lock_guard<std::mutex> lock_from(from.m, std::adopt_lock);
    std::lock_guard<std::mutex> lock_to(to.m, std::adopt_lock);

    // 执行转账(临界区)
    from.balance -= amount;
    to.balance += amount;

    // lock_guard 析构时自动 unlock(顺序无关)
}

int main() {
    Account A(1000), B(1000), C(1000);

    // 启动多个并发转账线程
    std::vector<std::thread> threads;

    // A ↔ B 转账
    threads.emplace_back([&]() {
        for (int i = 0; i < 500; ++i) {
            transfer(A, B, 1);
            transfer(B, A, 1);
        }
    });

    // B ↔ C 转账
    threads.emplace_back([&]() {
        for (int i = 0; i < 500; ++i) {
            transfer(B, C, 1);
            transfer(C, B, 1);
        }
    });

    // A ↔ C 转账
    threads.emplace_back([&]() {
        for (int i = 0; i < 500; ++i) {
            transfer(A, C, 1);
            transfer(C, A, 1);
        }
    });

    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }

    // 验证总金额守恒(应为 3000)
    int total = A.balance + B.balance + C.balance;
    std::cout << "A: " << A.balance << "\n";
    std::cout << "B: " << B.balance << "\n";
    std::cout << "C: " << C.balance << "\n";
    std::cout << "Total: " << total << " (should be 3000)\n";

    return 0;
}

2.超时机制

cpp 复制代码
std::timed_mutex mtx;
if (mtx.try_lock_for(std::chrono::seconds(1))) {
    // 成功获取锁
    mtx.unlock();
} else {
    // 超时,放弃或重试
}

3.按层级加锁

把应用程序分层,并且明确每个互斥位于哪个层级。若某线程已对低层级互斥加锁,则不准它再对高层级互斥加锁。具体做法是将层级的编号赋予对应层级应用程序上的互斥,并记录各线程分别锁定了哪些互斥:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

//层级锁
class hierarchical_mutex {
public:
    explicit hierarchical_mutex(unsigned long value) :_hierarchy_value(value),
        _previous_hierarchy_value(0) {}
    hierarchical_mutex(const hierarchical_mutex&) = delete;
    hierarchical_mutex& operator=(const hierarchical_mutex&) = delete;
    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;
    }
private:
    std::mutex  _internal_mutex;
    //当前层级值
    unsigned long const _hierarchy_value;
    //上一次层级值
    unsigned long _previous_hierarchy_value;
    //本线程记录的层级值
    static thread_local  unsigned long  _this_thread_hierarchy_value;
    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;
    }
};
thread_local unsigned long hierarchical_mutex::_this_thread_hierarchy_value(ULONG_MAX);
void test_hierarchy_lock() {
    hierarchical_mutex  hmtx1(1000);
    hierarchical_mutex  hmtx2(500);
    std::thread t1([&hmtx1, &hmtx2]() {
        hmtx1.lock();
        hmtx2.lock();
        hmtx2.unlock();
        hmtx1.unlock();
        });
    std::thread t2([&hmtx1, &hmtx2]() {
        hmtx2.lock();
        hmtx1.lock();
        hmtx1.unlock();
        hmtx2.unlock();
        });
    t1.join();
    t2.join();
}

层级锁能保证我们每个线程加锁时,一定是先加权重高的锁。

并且释放时也保证了顺序。

主要原理就是将当前锁的权重保存在线程变量中,这样该线程再次加锁时判断线程变量的权重和锁的权重是否大于,如果满足条件则继续加锁

4.无锁编程

  • 使用原子操作(std::atomic)、CAS 等
  • 高性能但复杂,适用于特定场景
相关推荐
元让_vincent2 小时前
DailyCoding C++ CMake | CMake 踩坑记:解决 ROS 项目中的“循环引用”与库链接依赖问题
c++·机器人·ros·动态库·静态库·cmake·循环引用
Nan_Shu_6142 小时前
学习: Blender 粒子篇
学习·blender
Pythonliu72 小时前
【February 组队学习【数学建模导论】~】
学习·数学建模
_OP_CHEN2 小时前
【前端开发之JavaScript】(二)JS基础语法上篇:吃透变量 / 类型 / 输入输出
开发语言·javascript·html·ecmascript·前端开发·网页开发
Maguyusi2 小时前
go 批量生成 c++与lua的proto文件
开发语言·后端·golang·protobuf
万能的小裴同学2 小时前
饥荒Mod
java·开发语言·junit
foxsen_xia2 小时前
Kamailio通过Lua写路由
开发语言·lua·信息与通信
燃于AC之乐2 小时前
深入解剖STL set/multiset:接口使用与核心特性详解
开发语言·c++·stl·面试题·set·multiset
REDcker2 小时前
Paho MQTT C 开发者快速入门
c语言·开发语言·mqtt