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 等
  • 高性能但复杂,适用于特定场景
相关推荐
xiaoxiaoxiaolll36 分钟前
Light: Sci & Appl. | 子阵列栅控HEMT超表面:太赫兹波前同时实现高速调制与物理层逻辑
学习
代码的小搬运工41 分钟前
UITableView
开发语言·ui·ios·objective-c
刚子编程44 分钟前
C# Join 深度解析:参数顺序、多表关联与空值处理最佳实践
开发语言·c#·最佳实践·join·多表关联·空值处理
AbandonForce1 小时前
哈希表(HashTable,散列表)个人理解
开发语言·数据结构·c++·散列表
爱喝水的鱼丶1 小时前
SAP-ABAP:SAP 与 ABAP 关联逻辑与入门路径:业务×开发的协作指南
服务器·前端·数据库·学习·sap·abap
代码中介商1 小时前
栈结构完全指南:顺序栈实现精讲
c语言·开发语言·数据结构
平凡但不平庸的码农1 小时前
Go 错误处理详解
开发语言·后端·golang
蓝桉~MLGT1 小时前
中级软考(软件工程师)常用错题整理(不间断更新)
学习·中级软考
样例过了就是过了1 小时前
LeetCode热题100 编辑距离
数据结构·c++·算法·leetcode·动态规划
z200509301 小时前
C++中位图和布隆过滤器的一些面试题
开发语言·c++