2022-08-02 C++并发编程(五)

本文深入探讨了C++并发编程中的死锁问题,通过示例解释了死锁产生的原因,如互斥锁导致的线程等待。提出了三种防范死锁的方法:1) 避免无锁死锁,如线程间避免互相等待;2) 一次性加锁,使用std::lock避免嵌套锁;3) 按照固定顺序获取锁,确保锁的获取有层次。文章强调了从设计阶段就应预防死锁的重要性。
摘要由CSDN通过智能技术生成


前言

互斥锁对于多线程并发编程可以很好的保护数据,但有时效果会出乎意料,比如死锁。


一、死锁

死锁的条件很简单,如同打麻将,四个人都缺同一张牌就和牌,但四个人都不会打出这一张牌,于是就是无尽的消耗。

以下示例是其中一种典型情况,交换两个元素。通常不会有任何问题,但多线程中,如果线程1要交换A,B,线程2要交换B,A,此时为了保护数据,需要加锁,但加锁后则极为尴尬。

顺序加锁的结果是线程1持有锁A,等待锁B,线程2持有锁B,等待锁A,互不相让,就好像麻将桌上等同一张牌。

所以逻辑上必须同时加锁:

#include <iostream>
#include <mutex>
#include <thread>

struct someBigObject
{
    someBigObject() = default;

    explicit someBigObject(int64_t rhs)
        : l(rhs)
    {}

    void prt() const
    {
        std::cout << l << std::endl;
    }

  private:
    friend void swap(someBigObject &lhs, someBigObject &rhs);
    int64_t l = 0;
};

void swap(someBigObject &lhs, someBigObject &rhs)
{
    std::swap(lhs.l, rhs.l);
}

struct X
{
    explicit X(const someBigObject &sd)
        : someDetail(sd)
    {}

  private:
    friend void swap(X &lhs, X &rhs);
    someBigObject someDetail;
    std::mutex m;
};

void swap(X &lhs, X &rhs)
{
    if (&lhs == &rhs)
    {
        return;
    }

    std::lock(lhs.m, rhs.m);

    std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
    std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);

    // c++17
    // std::scoped_lock guard(lhs.m, rhs.m);

    swap(lhs.someDetail, rhs.someDetail);
}

auto main() -> int
{
    someBigObject l(8);
    someBigObject m(9);
    X lx(l);
    X mx(m);
    swap(lx, mx);
    return 0;
}

二、防范死锁

1.无锁死锁

可能有些意外,没有锁也能导致死锁,例如线程间的相互等待。

比如两个thread 对象,在相互调用join(),但此时又是回到缺一种拍的麻将桌,互相等,谁也不出牌。

不过好在一般人不会这么设计程序,但也难说。

#include <iostream>
#include <mutex>
#include <thread>

void prt(std::thread &threadX)
{
    {
        std::cout << "begin\n";
    }

    threadX.join();
}

extern std::thread threadB;

std::thread threadA(prt, std::ref(threadB));
std::thread threadB(prt, std::ref(threadA));

void funcA()
{
    std::cout << "funcA" << std::endl;
    threadB.join();
}

void funcB()
{
    std::cout << "funcB" << std::endl;
    threadA.join();
}

auto main() -> int
{
    std::thread threadC = std::thread(funcA);
    std::thread threadD = std::thread(funcB);

    threadC.join();
    threadD.join();

    std::cout << "OK" << std::endl;

    return 0;
}

2.避免嵌套锁

最简单的防止死锁方法是每个线程只持有一个锁,此时就不会存在锁的竞争,如果一定要持有多个锁,也要一次性的用std::lock()同时加锁,防止死锁。

3.依从固定顺序获取锁

当不可避免的使用多个锁,并且封装在不同层次,则务必按照层级次第加锁。

比如要删除一个共享双向链表的某个节点,一个线程从双向链表的左侧加锁,一个线程从双向链表的右侧加锁,最终一定会死锁,所以要从逻辑上杜绝这种设计,如果一定要遍历途中加锁,那么就只能从一个方向加。

在设计之初,划分锁的层级,可以封装出层级锁,虽然不能阻止设计出不按层级加锁的程序,但只要这种不按层次加锁的程序运行,就会抛出异常。

封装时,为了保证每个线程拥有自己的层级,需要设置线程专属变量,

    static thread_local uint32_t thisThreadLevelVal;

与以往所有变量不同,这时一个由两个关键字修饰的变量,会建立每个线程独立的变量。

作为共享的层级锁类,如果不加thread_local,则每个线程的层级锁对象的层级改动都会牵扯其他线程,也就无法比较分级了。

以下代码示范 std::thread tb(threadB) 不按层级加锁,外层低levelMutex otherMutex(6000),内层高levelMutex highLevelMutex(10000),抛出异常:

#include <iostream>
#include <mutex>
#include <thread>

struct levelMutex
{
    explicit levelMutex(uint32_t value)
        : levelVal(value)
        , preLevelVal(0)
    {}

    void lock()
    {
        checkForLevelErr();
        inMutex.lock();
        updateLevelVal();
    }

    void unlock()
    {
        if (thisThreadLevelVal != levelVal)
        {
            throw std::logic_error("mutex level err");
        }
        thisThreadLevelVal = preLevelVal;
        thisValPtr = thisThreadLevelVal;
        inMutex.unlock();
    }

    auto try_lock() -> bool
    {
        checkForLevelErr();
        if (!inMutex.try_lock())
        {
            return false;
        }
        updateLevelVal();
        return true;
    }

  private:
    void checkForLevelErr() const
    {
        if (thisThreadLevelVal <= levelVal)
        {
            throw std::logic_error("mutex level err");
        }
    }

    void updateLevelVal()
    {
        preLevelVal = thisThreadLevelVal;
        thisThreadLevelVal = levelVal;
        thisValPtr = thisThreadLevelVal;
        fprintf(stdout, "%d\n", thisThreadLevelVal);
    }

    //内部互斥量
    std::mutex inMutex;
    //等级值
    uint32_t const levelVal;
    //前一个等级值
    uint32_t preLevelVal;
    //本线程等级值
    static thread_local uint32_t thisThreadLevelVal;

    uint32_t thisValPtr = ULONG_MAX;
};

thread_local uint32_t levelMutex::thisThreadLevelVal(ULONG_MAX);

levelMutex highLevelMutex(10000);
levelMutex lowLevelMutex(5000);
levelMutex otherMutex(6000);

auto doLowLevelStuff() -> int
{
    return 1;
}

auto lowLevelFunc() -> int
{
    std::lock_guard<levelMutex> lk(lowLevelMutex);
    return doLowLevelStuff();
}

void highLevelStuff(int someParam)
{
    std::cout << someParam << std::endl;
}

void highLevelFunc()
{
    std::lock_guard<levelMutex> lk(highLevelMutex);
    highLevelStuff(lowLevelFunc());
}

void threadA()
{
    highLevelFunc();
}

void doOtherStuff()
{
    std::cout << "other" << std::endl;
}

void otherStuff()
{
    highLevelFunc();
    doOtherStuff();
}

void threadB()
{
    std::lock_guard<levelMutex> lk(otherMutex);
    otherStuff();
}

auto main() -> int
{
    std::thread ta(threadA);
    std::thread tb(threadB);
    ta.join();
    tb.join();
    return 0;
}

总结

多线程死锁问题通常较难解决,很多时候是偶发出现,让人摸不着头脑,所以从设计开始,就应加以注意。

参考文献:C++并发编程实战(第2版)[英] 安东尼•威廉姆斯(Anthony Williams)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不停感叹的老林_<C 语言编程核心突破>

不打赏的人, 看完也学不会.

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值