• C++多线程学习06 利用RAII


    RAII是C++的发明者Bjarne Stroustrup提出的概念,RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。即使用局部资源来管理对象,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。

    一、手动实现RAII管理mutex资源

    为什么需要用RAII来管理:
    除去忘了写解锁语句这种低级错误,还可能加锁之后抛出异常,无法正常解锁

    在RAII的指导下,我们应该使用(XMutex类)来管理资源,将资源(mux)和对象lock的生命周期绑定:
    在要使用RAII机制的线程中定义一个XMutex类对象lock,XMutex类接受一个mutex类型的参数,其中有一个指向mutex的引用mux_,在lock执行其构造函数时通过对mux_加锁来对传进来的mux加锁,在该线程退出时lock执行其析构函数,通过对mux_解锁来对mux解锁,从而实现使用类来管理资源,将资源和对象的生命周期绑定。

    using namespace std;
    // RAII
    class XMutex
    {
    public:
        XMutex(mutex& mux):mux_(mux)
        {
            cout << "Lock" << endl;
            mux_.lock();
        }
        ~XMutex()
        {
            cout << "Unlock" << endl;
            mux_.unlock();
        }
    private:
        mutex& mux_;
    };
    static mutex mux;
    void TestMutex(int status)
    {
        XMutex lock(mux);
        if (status == 1)
        {
            cout << "=1" << endl;
            return;
        }
        else
        {
            cout << "!=1" << endl;
            return;
        }
    }
    int main(int argc, char* argv[])
    {
        TestMutex(1);
        TestMutex(2);
        
        getchar();
        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

    类成员是引用的话要在构造函数中使用初始化列表
    在开始执行构造函数的函数体之前,要完成初始化。初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中,这是因为如果没有在构造函数初始值列表中显示地初始化成员,则该成员将在构造函数体之前执行默认初始化。之后,再进入构造函数体{}中。对于引用与const类型的成员,他们是很专一的,如果进行了默认初始化,他们指向的内容将不能改变,因此需要使用初始化列表。

    由多种条件退出的话还要在每个退出前加上一个解锁,通过RAII来管理的话可以放心的退出,不用去操心解锁了。

    可以看到,在测试线程中即使没有手动地解锁,也能调用unlock:
    在这里插入图片描述

    二、c++11支持的RAII管理互斥资源

    C++11 实现严格基于作用域的互斥体所有权包装器lock_guard
    sd:loco_ guard类采用RAII手法管理某个锁对象,启动时在对象构造时将mutex加锁,无需手动调用lock)方法,析构时对mutex解锁,这样保证在异常的情况下mutex可以在lock guard对象析构时被解锁,不会阻塞其它线程获mutex.

    先来看看包装器lock_guard的定义:

    template <class _Mutex>
    class lock_guard { 
    // class with destructor that unlocks a mutex
    //用解除互斥锁的析构函数类
    public:
        using mutex_type = _Mutex;
    
        explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
            _MyMutex.lock();
        }
    
        lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) { // construct but don't lock
        }
    
        ~lock_guard() noexcept {
            _MyMutex.unlock();
        }
    
        lock_guard(const lock_guard&) = delete;
        lock_guard& operator=(const lock_guard&) = delete;
    
    private:
        _Mutex& _MyMutex;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    定义分析:
    01lock_guard是一个模板来,可以接受各种类型的互斥量,如一般的互斥量,超时互斥量,共享互斥量等
    02
    explicit类型的构造函数和普通类型的构造函数的区别
    普通的构造函数可以被显式调用和隐试调用,但是explicit的构造函数只能被显式的调用,不能被隐试的调用
    llock_guard lock1(gmutex);显式调用
    lock_guard lock2=lock1;隐式调用

    lock_guard的第一个构造函数接收一个参数,只能显式调用
    lock_guard的第二个构造函数接收两个参数,第二个参数adopt_lock的意思的收养锁(接管之前上好的锁),然后由lock对象来完成对lock的抚养工作(解锁),如果该代码之前没有上锁会报错:
    在这里插入图片描述

    03其拷贝构造以及重载的=被删除了,即不能使用lock_guard lock2=lock1或者lock_guard lock2(lock1),即

    using namespace std;
    // RAII
    
    static mutex gmutex;
    void TestLockGuard(int i)
    {
        gmutex.lock();
        {
            //已经拥有锁,不lock
            lock_guard<mutex> lock(gmutex,adopt_lock);
            //结束释放锁
        }
        {
            lock_guard<mutex> lock(gmutex);
            cout << "begin thread " << i << endl;
        }
        for (;;)
        {
            {
                lock_guard<mutex> lock(gmutex);
                cout << "In " << i << endl;
            }
            this_thread::sleep_for(500ms);
        }
    }
    int main(int argc, char* argv[])
    {
        for (int i = 0; i < 3; i++)
        {
            thread th(TestLockGuard, i + 1);
            th.detach();
        }
    
        getchar();
        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

    临界区不一定包括整个函数,因为在锁住的临界区不应该有sleep,sleep的时候锁还在,其他线程进不来,因此使用{}来控制锁的临界区:

     for (;;)
        {
            {
                lock_guard<mutex> lock(gmutex);
                cout << "In " << i << endl;
            }
            this_thread::sleep_for(500ms);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在进到{}中时通过lock_guard类对象的构造来对gmutex加锁,在退出{}时通过lock_guard类对象的析构来对gmutex解锁
    在这里插入图片描述
    可以看到123号线程依次加锁,cout,解锁

  • 相关阅读:
    使用Qt实现命令行解析
    DSA之图(1):什么是图
    Linux服务器占用处理手记
    06.位置匹配 (Python)
    在Cisco设备上配置接口速度和双工
    Kitchen Racks
    黑客大牛是怎样练成的?
    使用GPA和夜神模拟器实现K帧
    C++类与对象——封装
    2022年上半年系统分析师上午真题及答案解析
  • 原文地址:https://blog.csdn.net/qq_42567607/article/details/125552032