• c++中互斥量mutex的使用,实现线程同步


    一.std::mutex 介绍

    下面以 std::mutex 为例介绍 C++11 中的互斥量用法。

    std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。

    1.1 std::mutex 的成员函数

    • 构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
    • lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
    • unlock(), 解锁,释放对互斥量的所有权。
    • try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

    1.2 std::mutex 代码示例

    mutex.cpp

    #include        // std::cout
    #include          // std::thread
    #include           // std::mutex
    
    volatile int counter(0);  // non-atomic counter
    std::mutex mutex;         // locks access to counter
    
    //定义线程执行函数
    void thread_func() 
    {
        mutex.lock();  //对mutex上锁(如果mutex已经被占用,则阻塞在此处)
        ++counter; //执行操作
        mutex.unlock();//对mutex解锁
    }
    
    int main (int argc, const char* argv[]) 
    {
        mutex.lock();                        //对mutex上锁
        std::thread subthread(thread_func);  //创建一个子线程,线程执行函数为thread_func
        std::cout<<"before counter = "<<counter<<std::endl;   //解锁前
        mutex.unlock();                      //对mutex解锁(启动子线程执行)
        if (subthread.joinable()) {          //回收子线程
            subthread.join();
            std::cout<<"子线程已回收"<<std::endl;
        }
        std::cout<<"after counter = "<<counter<<std::endl;   //解锁前
        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

    编译 && 执行:

    g++ mutex.cpp -lpthread’
    ./a.out

    执行结果:
    在这里插入图片描述

    二.std::time_mutex 介绍

      std::time_mutex 比 std::mutex 多了两个成员函数,try_lock_for()try_lock_until()

      try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false

      try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false

    2.1 std::time_mutex 代码示例

    time_mutex.cpp

    #include        // std::cout
    #include          // std::thread
    #include           // std::mutex
    
    volatile int counter(0);  // non-atomic counter
    std::timed_mutex mutex;         // locks access to counter
    
    //定义线程执行函数
    void thread_func() 
    {
        while (!mutex.try_lock_for(std::chrono::milliseconds(499))) {
            std::cout << "子线程等待中..."<<std::endl;
        }
        ++counter; //执行操作
        mutex.unlock();//对mutex解锁
    }
    
    int main (int argc, const char* argv[]) 
    {
        mutex.lock();                        //对mutex上锁
        std::thread subthread(thread_func);  //创建一个子线程,线程执行函数为thread_func
        std::cout<<"before counter = "<<counter<<std::endl;   //解锁前
        std::this_thread::sleep_for(std::chrono::milliseconds(2000));  //主线程阻塞执行2s
        mutex.unlock();                      //对mutex解锁(启动子线程执行)
        if (subthread.joinable()) {          //回收子线程
            subthread.join();
            std::cout<<"子线程已回收"<<std::endl;
        }
        std::cout<<"after counter = "<<counter<<std::endl;   //解锁前
        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

    编译 && 执行:

    g++ time_mutex.cpp -lpthread’
    ./a.out

    执行结果:
    在这里插入图片描述

    三.std::lock_guard介绍

      std::lock_guard是C++11提供的锁管理器,可以管理std::mutex,也可以管理其他常见类型的锁。
      std::lock_guard的对锁的管理属于RAII风格用法(Resource Acquisition Is Initialization),在构造函数中自动绑定它的互斥体并加锁,在析构函数中解锁,大大减少了死锁的风险。

    3.1 std::lock_guard 代码示例

    lock_guard.cpp

    #include 
    #include 
    #include 
    
    int loop_cnt = 10;
    
    class Widget
    {
    public:
    	Widget() = default;
    	~Widget() = default;
    
    	void addCnt()
    	{
    		std::lock_guard<std::mutex> cLockGurad(lock_); //构造时加锁,析构时解锁
            std::cout<<"线程执行addCnt函数"<<std::endl;
    
    		// lock_.lock(); //不使用lock_guard时的写法
            for (int i = 0; i < loop_cnt; i++)
            {
                cnt++;
    	        std::cout << "cnt =" << cnt << std::endl;
            }
    		// lock_.unlock();//不使用lock_guard时的写法,万一没有解锁就会死锁。
    	}
    
    	int cnt = 0;        //类成员函数,属于对象
    
    private:
    	std::mutex lock_;  //mutex定义为类成员变量
    };
    
    void ThreadMain1(Widget *pw)
    {
    	std::cout << "thread 1 begin." << std::endl;
    	pw->addCnt();
    	std::cout << "thread 1 end."  << std::endl;
    }
    
    void ThreadMain2(Widget *pw)
    {
    	std::cout << "thread 2 begin."  << std::endl;
        pw->addCnt();
    	std::cout << "thread 2 end."  << std::endl;
    }
    
    int main()
    {
    	Widget *pw = new Widget();   //定义一个对象指针
    
    	std::thread t1(&ThreadMain1, pw);  //开启两个子线程
    	std::thread t2(&ThreadMain2, pw);
    
    	t1.join();  //回收;两个子线程
    	t2.join();
    
    	std::cout << "cnt =" << pw->cnt << std::endl;
    	
    	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

    编译 && 执行:

    g++ lock_guard.cpp -lpthread’
    ./a.out

    执行结果:
    在这里插入图片描述
      当把mutex加锁语句屏蔽之后再进行测试,修改如下:

    	void addCnt()
    	{
    		//std::lock_guard cLockGurad(lock_); //构造时加锁,析构时解锁
            std::cout<<"线程执行addCnt函数"<<std::endl;
    
    		// lock_.lock(); //不使用lock_guard时的写法
            for (int i = 0; i < loop_cnt; i++)
            {
                cnt++;
    	        std::cout << "cnt =" << cnt << std::endl;
            }
    		// lock_.unlock();//不使用lock_guard时的写法,万一没有解锁就会死锁。
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    执行结果:
    在这里插入图片描述
      可以看出在加锁的情况下(上图),两个线程是互斥的执行对象的addCnt()函数,当线程1执行完并且退出解锁后,线程2才可以执行addCnt()函数,通过上述的加锁(mutex)机制可以保证addCnt()函数是线程安全的。
      可以看出在不加锁的情况下(下图),两个线程是交叉执行的,对于cnt共有变量的处理结果是未知的,因此addCnt()函数是非线程安全的。
      总结:因此对于同一个类对象在不同线程中执行时需要考虑对共享变量的原子化处理,同时考虑成员函数的线程安全执行,通常考虑在类中定义mutex变量,通过mutex互斥变量的加锁实现函数的多线程安全执行。

    四.std::unique_lock介绍

      std::unique_lock为锁管理模板类,是对通用mutex的封装。std::unique_lock对象以独占所有权的方式(unique owership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁unique_lock具有lock_guard的所有功能,而且更为灵活。虽然二者的对象都不能复制,但是unique_lock可以移动(movable),因此用unique_lock管理互斥对象,可以作为函数的返回值,也可以放到STL的容器中。
      std::unique_lock还支持同时锁定多个mutex,这避免了多道加锁时的资源”死锁”问题。在使用std::condition_variable时需要使用std::unique_lock而不应该使用std::lock_guard

    std::unique_lock类成员函数介绍:

    (1). unique_lock构造函数:禁止拷贝构造,允许移动构造;

    (2). operator =:赋值操作符,允许移动赋值,禁止拷贝赋值;

    (3). operator bool:返回当前std::unique_lock对象是否获得了锁;

    (4). lock函数:调用所管理的mutex对象的lock函数;

    (5). try_lock函数:调用所管理的mutex对象的try_lock函数;

    (6).try_lock_for函数:调用所管理的mutex对象的try_lock_for函数;

    (7).try_lock_until函数:调用所管理的mutex对象的try_lock_until函数;

    (8). unlock函数:调用所管理的mutex对象的unlock函数;

    (9). release函数:返回所管理的mutex对象的指针,并释放所有权,但不改变mutex对象的状态;

    (10). owns_lock函数:返回当前std::unique_lock对象是否获得了锁;

    (11). mutex函数:返回当前std::unique_lock对象所管理的mutex对象的指针;

    (12). swap函数:交换两个unique_lock对象。

    4.1 std::unique_lock代码示例

    #include 
    #include 
    #include 
    
    int loop_cnt = 10;
    
    class Widget
    {
    public:
    	Widget() = default;
    	~Widget() = default;
    
    	void addCnt()
    	{
    		std::unique_lock<std::mutex> uniquelock(lock_); //构造时加锁,析构时解锁
            std::cout<<"线程执行addCnt函数"<<std::endl;
    
    		// lock_.lock(); //不使用lock_guard时的写法
            for (int i = 0; i < loop_cnt; i++)
            {
                cnt++;
    	        std::cout << "cnt =" << cnt << std::endl;
            }
    		// lock_.unlock();//不使用lock_guard时的写法,万一没有解锁就会死锁。
    	}
    
    	int cnt = 0;        //类成员函数,属于对象
    
    private:
    	std::mutex lock_;  //mutex定义为类成员变量
    };
    
    void ThreadMain1(Widget *pw)
    {
    	std::cout << "thread 1 begin." << std::endl;
    	pw->addCnt();
    	std::cout << "thread 1 end."  << std::endl;
    }
    
    void ThreadMain2(Widget *pw)
    {
    	std::cout << "thread 2 begin."  << std::endl;
        pw->addCnt();
    	std::cout << "thread 2 end."  << std::endl;
    }
    
    int main()
    {
    	Widget *pw = new Widget();   //定义一个对象指针
    
    	std::thread t1(&ThreadMain1, pw);  //开启两个子线程
    	std::thread t2(&ThreadMain2, pw);
    
    	t1.join();  //回收;两个子线程
    	t2.join();
    
    	std::cout << "cnt =" << pw->cnt << std::endl;
    	
    	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

    4.2 std::unique_lock与std::lock_guard的区别分析

      unique_locklock_guard都是管理锁的辅助类工具,都是RAII风格;它们是在定义时获得锁,在析构时释放锁。它们的主要区别在于unique_lock锁机制更加灵活,可以在需要的时候进行lock或者unlock调用,不非得是析构或者构造时。它们的区别可以通过成员函数就可以一目了然。
    在这里插入图片描述
      另外,在条件变量condition_variable中只能使用std::unique_lock

  • 相关阅读:
    Java学习笔记(四)——程序控制结构
    C语言指针详解(4)———找工作必看指针笔试题汇总
    Web前端 | HTML基本标签、实体符号、表格、超链接、列表
    Workfine新手入门:日期间隔函数范围判断
    torchvision.datasets.ImageFolder前的数据整理及使用方法
    CentOS7.9安装
    提取多个txt数据并合成excel——例子:与中国建交的国家
    2022.11.30-----leetcode.895
    PT_连续型随机变量/分布函数/概率密度
    27.3 Java集合学习之List(基本概念,API,存储原理)
  • 原文地址:https://blog.csdn.net/weixin_42700740/article/details/126154807