zoukankan      html  css  js  c++  java
  • 【C++11 多线程】lock_guard与unique_lock(五)

    一、前言

    如果g_mutex.lock()g_mutex.unlock()之间的语句发生了异常,会发生什么?看这个例子:

    #include <iostream>
    #include <thread>
    #include <mutex>
    
    // 实例化互斥锁对象,不要理解为定义变量
    std::mutex g_mutex;
    
    // 使用锁保护,创建一个线程安全的打印函数
    void safePrint(std::string msg, int val) {
        g_mutex.lock(); // 上锁
    
        std::cout << msg << val << std::endl;
    
        // 模拟程序中的异常,实际异常要复杂的多
        if (val == -2)
            return;
    
        g_mutex.unlock(); // 解锁
    }
    
    void threadTask()
    {
        for (int i = 0; i < 10; i++)
            safePrint("print thread: ", i);
    }
    
    int main()
    {
        std::thread t(threadTask);
        for (int i = 0; i > -10; i--)
            safePrint("print main: ", i);
        t.join();
    
        return 0;
    }
    
    /*
    输出:
    print main: 0
    print main: -1
    print main: -2
    然后就报错:abort() has been called
    */
    

    g_mutex.unlock()语句没有机会执行!导致导致g_mutex一直处于锁着的状态,其他使用safePrint函数的线程就会阻塞,甚至可能导致程序崩溃。

    解决这个问题也很简单,使用 C++ 中常见的RAII技术,即获取资源即初始化(Resource Acquisition Is Initialization)技术,这是 C++ 中管理资源的常用方式。简单的说就是在类的构造函数中创建资源,在析构函数中释放资源,这样就算发生了异常, C++ 也能保证类的析构函数能够执行。

    我们不需要自己写个类包装mutex, C++ 库已经提供了应用RAII技术的std::lock_guard类模板。

    二、std::lock_guard

    std::lock_guard原理是:声明一个局部的std::lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:创建即加锁,作用域结束自动解锁。从而使std::lock_guard()就可以替代lock()unlock()

    void safePrint(std::string msg, int val) {
        // 用此语句替换了g_mutex.lock(),参数为互斥锁g_mutex
        std::lock_guard<std::mutex> guard(g_mutex);
        std::cout << msg << val << std::endl;
    } // 此时不需要写g_mutex.unlock(),guard出了作用域被释放,自动调用析构函数,于是g_mutex被解锁
    

    需要互斥访问共享资源的那段代码称为临界区,临界区范围应该尽可能的小,即 lock 互斥量后应该尽早 unlock,通过使用 {} 来调整作用域范围,可使得互斥量 g_mutex 在合适的地方被解锁

    void safePrint(std::string msg, int val) {
        {
            // 用此语句替换了g_mutex.lock(),参数为互斥锁g_mutex
            std::lock_guard<std::mutex> guard(g_mutex);
            std::cout << msg << val << std::endl;
        } // 通过使用{}来调整作用域范围,可使得g_mutex在合适的地方被解锁
        
        std::cout << "作用域外的内容" << std::endl;    
    }
    

    推荐使用std::lock_guard,这样可以防止因为异常无法解锁或者程序员自己忘记解锁。

    三、std::unique_lock

    互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域,也就是使用细粒度锁

    这一点lock_guard做的不好,不够灵活,lock_guard只能保证在析构的时候执行解锁操作,lock_guard本身并没有提供加锁和解锁的接口,但是有些时候会有这种需求。看下面的例子:

    class LogFile {
        std::mutex m_mutex;
        std::ofstream f;
    
    public:
        LogFile() {
            f.open("log.txt");
        }
    
        ~LogFile() {
            f.close();
        }
    
        void safePrint(std::string msg, int val) {
            {
                std::lock_guard<std::mutex> guard(m_mutex);
                // do something 1
            }
    
            // do something 2
            {
                std::lock_guard<std::mutex> guard(m_mutex);
                // do something 3
                f << msg << val << std::endl;
                std::cout << msg << val << std::endl;
            }
        }
    };
    

    上面的代码中,一个函数内部有两段代码需要进行保护,这个时候使用lock_guard就需要创建两个局部对象来管理同一个互斥锁(其实也可以只创建一个,但是锁的粒度太大,效率不行),修改方法是使用unique_lock。它提供了lock()unlock()接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁(lock_guard就一定会解锁)。上面的代码修改如下:

    class LogFile {
        std::mutex m_mutex;
        std::ofstream f;
    
    public:
        LogFile() {
            f.open("log.txt");
        }
    
        ~LogFile() {
            f.close();
        }
    
        void safePrint(std::string msg, int val) {
            std::unique_lock<std::mutex> guard(m_mutex);
            // do something 1
            guard.unlock(); // 临时解锁
    
            // do something 2
            guard.lock(); //继续上锁
    
            // do something 3
            f << msg << val << std::endl;
            std::cout << msg << val << std::endl;
            // 结束时析构guard会临时解锁
            // 这句话可要可不要,不写,析构的时候也会自动执行
            // guard.ulock();
        }
    };
    

    上面的代码可以看到,在无需加锁的操作时,可以先临时释放锁,然后需要继续保护的时候,可以继续上锁,这样就无需重复的实例化lock_guard对象,还能减少锁的区域。同样,可以使用参数std::defer_lock设置初始化的时候不进行默认的上锁操作:

    void safePrint(std::string msg, int val) {
        std::unique_lock<std::mutex> guard(m_mutex, std::defer_lock);
    
        //do something 1
        guard.lock();
        // do something protected
        guard.unlock(); // 临时解锁
    
        // do something 2
        guard.lock(); //继续上锁
        
        // do something 3
        f << msg << val << std::endl;
        std::cout << msg << val << std::endl;
        // 结束时析构guard会临时解锁
    }
    

    这样使用起来就比lock_guard更加灵活!然后这也是有代价的,因为它内部需要维护锁的状态,所以效率要比lock_guard低一点,在lock_guard能解决问题的时候,就是用lock_guard,反之,使用unique_lock

    另外,请注意,unique_locklock_guard都不能复制,lock_guard不能移动,但是unique_lock可以!

    // unique_lock 可以移动,不能复制
    std::unique_lock<std::mutex> guard1(g_mutex);
    std::unique_lock<std::mutex> guard2 = guard1;  // error
    std::unique_lock<std::mutex> guard2 = std::move(guard1); // ok
    
    // lock_guard 不能移动,不能复制
    std::lock_guard<std::mutex> guard1(g_mutex);
    std::lock_guard<std::mutex> guard2 = guard1;  // error
    std::lock_guard<std::mutex> guard2 = std::move(guard1); // error
    

    参考:

    [c++11]多线程编程(五)——unique_lock

    C++11多线程并发基础入门教程


  • 相关阅读:
    Java实现 洛谷 P1423 小玉在游泳
    Java设置session超时(失效)的时间
    How Tomcat works — 八、tomcat中的session管理
    三种常用的MySQL建表语句
    mysql和oracle的区别(功能性能、选择、使用它们时的sql等对比)
    oracle 基础表 mysql版
    oracle员工表和部门表基本操作
    Oracle
    java生成6位随机数
    用Ajax图片上传、预览、修改图片
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/14576646.html
Copyright © 2011-2022 走看看