• 2022-08-03 C++并发编程(六)



    前言

    锁的归属权转移也是一个值得研究的事情,比如我们处理一个数据,需要在不同函数中顺序处理,这是非常正常的事情,毕竟一个函数的功能有限。

    但在多线程中,有这么个问题,每个函数都是线程安全的,但是一旦组合,就不是线程安全的。为了整体的线程安全,需要不停的将锁的所有权转移给下一个承接的函数。

    同时控制锁的颗粒度,对于性能提升及其有意义,可防止因锁操作问题,导致多线程效率不如单线程效率。


    一、可转移所有权的锁

    std::unique_lock 类是一种可移动但不可复制的锁,其最基本的使用,加锁,和 std::lock_guard 类完全相同。

    但正如标题所言,std::unique_lock 可以转移所有权,也就是从一个函数传给另一个函数,如同其他所有权类一样,它可以移动,不可复制。

    #include 
    #include 
    
    struct someBigObject
    {
        someBigObject(int aa, int bb)
            : a(aa)
            , b(bb)
        {}
        int a = 0;
        int b = 0;
    };
    
    void swap(someBigObject &lhs, someBigObject &rhs)
    {
        std::swap(lhs.a, rhs.a);
        std::swap(lhs.b, rhs.b);
    }
    
    struct X
    {
        explicit X(someBigObject &sd)
            : someDetail(sd)
        {}
        friend void swap(X &lhs, X &rhs)
        {
            if (&lhs == &rhs)
            {
                return;
            }
    
            //利用RAII自动解锁
            //因为std::unique_lock类具有成员函数lock()、try_lock()和unlock(),
            //所以它的实例得以传给std::lock()函数
            // defer_lock: 延迟上锁
            // unique_lock: 可移动不可复制的锁
            std::unique_lock<std::mutex> lockA(lhs.m, std::defer_lock);
            std::unique_lock<std::mutex> lockB(rhs.m, std::defer_lock);
            std::lock(lockA, lockB);
    
            //以上代码同如下三行:
            // adopt_lock: 采用锁
            // 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);
    
            //以上代码同如下一行:
            // scoped_lock: 范围锁
            // std::scoped_lock guard(lhs.m, rhs.m);
    
            swap(lhs.someDetail, rhs.someDetail);
        }
    
      private:
        someBigObject someDetail;
        std::mutex m;
    };
    
    auto main() -> int
    {
        someBigObject testA(3, 2);
        someBigObject testB(8, 0);
        X testXa(testA);
        X testXb(testB);
        swap(testXa, testXb);
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    二、锁的转移

    对于 std::unique_lock 类的对象,可以通过函数返回值进行转移,也可以通过 std::move() 进行显示转移。

    通过转移锁函数,将处理数据的函数进行封装,然后不停的转移锁,可以一直保证并发安全,即线程的数据安全。

    #include 
    #include 
    #include 
    
    void prepareData()
    {
        std::cout << "prepareData" << std::endl;
    }
    
    void doSomething()
    {
        std::cout << "doSomething" << std::endl;
    }
    
    auto getLock() -> std::unique_lock<std::mutex>
    {
        extern std::mutex someMutex;
        std::unique_lock<std::mutex> lk(someMutex);
        prepareData();
        return lk;
    }
    
    void processData()
    {
        std::unique_lock<std::mutex> lk(getLock());
        // guard: 警卫
        // std::lock_guard不可转移
        doSomething();
    }
    
    auto main() -> int
    {
        processData();
        return 0;
    }
    
    std::mutex someMutex;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    三、控制锁的颗粒度

    加锁可以保护多线程并发操作数据的安全,然而也必然影响程序的效率,严谨的程序员需要做到,只在必要的地方加锁,对加锁的边界保持敏感。

    以下代码简单模拟了一种处理过程,在获取数据时加锁,处理数据时解锁,写入结果时加锁,精细控制锁的边界。

    struct someClass
    {
        someClass() = default;
    
      private:
        int a = 0;
    };
    
    auto getNextDataChunk() -> someClass &
    {
        static someClass a;
        return a;
    }
    
    struct resultType
    {
        explicit resultType(someClass aa)
            : a(aa)
        {}
    
      private:
        someClass a;
    };
    
    auto process(someClass a) -> resultType &
    {
        static resultType b(a);
        return b;
    }
    
    void writeResult(someClass & /*unused*/, resultType & /*unused*/)
    {
        std::cout << "write someClass and resultType" << std::endl;
    }
    
    void getAndProcessData()
    {
        extern std::mutex theMutex;
        std::unique_lock<std::mutex> myLock(theMutex);
        someClass dataToProcess = getNextDataChunk();
        myLock.unlock();
        resultType result = process(dataToProcess);
        myLock.lock();
        writeResult(dataToProcess, result);
    }
    
    auto main() -> int
    {
        getAndProcessData();
        return 0;
    }
    
    std::mutex theMutex;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    控制锁的颗粒度有时在不经意间,会引起语义的错误,比如以下类对象的 == 比较。

    此时的语义就不是真正的同一时刻两个对象值相等,而是某个时刻的A和另一时刻的B是否相等。

    这是控制锁的颗粒度时,考虑不周导致的逻辑错误,需要小心防范。

    struct Y
    {
        explicit Y(int sd)
            : someDetail(sd)
        {}
        friend auto operator==(const Y &lhs, const Y &rhs) -> bool
        {
            if (&lhs == &rhs)
            {
                return true;
            }
            const int lhsValue = lhs.getDetail();
            const int rhsValue = rhs.getDetail();
            return rhsValue == lhsValue;
        }
    
      private:
        int someDetail = 0;
        mutable std::mutex m;
        auto getDetail() const -> int
        {
            std::lock_guard<std::mutex> lockA(m);
            return someDetail;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    总结

    通过锁的所有权转移,延续线程的函数对数据的控制,起到保护作用,使得并发安全。

    通过控制锁的颗粒度,防止性能的下降,但要注意,不要因为过细的颗粒度导致逻辑错误,引起线程并发不安全。

  • 相关阅读:
    企业在申请专利时如何明确要申请专利的技术点
    专为大模型训练优化,百度集合通信库 BCCL 万卡集群快速定位故障
    WebServer(Nginx、Httpd、IIS)搭建Http文件服务
    SpringCloudGateWay个人笔记
    Opencv 极坐标变换
    LeetCode 刷题 [C++] 第98题.验证二叉搜索树
    [剑指Offer] 两种方法求解空格替换
    elementUI 图片全屏预览
    花 1 万块做付费咨询,值得吗?
    使用宝塔面板在Linux上搭建网站,并通过内网穿透实现公网访问
  • 原文地址:https://blog.csdn.net/m0_54206076/article/details/126133624