栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

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

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

C++并发编程(五)
  • 前言
  • 一、死锁
  • 二、防范死锁
    • 1.无锁死锁
    • 2.避免嵌套锁
    • 3.依从固定顺序获取锁
  • 总结


前言

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


一、死锁

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

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

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

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

#include 
#include 
#include 

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 lock_a(lhs.m, std::adopt_lock);
    std::lock_guard 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 
#include 
#include 

void prt(std::thread &threadX)
{
    {
        std::cout << "beginn";
    }

    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 
#include 
#include 

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, "%dn", 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 lk(lowLevelMutex);
    return doLowLevelStuff();
}

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

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

void threadA()
{
    highLevelFunc();
}

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

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

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

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

总结

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

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1034670.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号