• 13 C++11 线程同步之互斥锁


    1. std::mutex

    1.1 lock()、trylock()、unlock()

    lock()函数给临界区加锁,并且只有一个线程获得锁的所有权,它具有阻塞线程的作用;
    try_lock()同样也是用于给临界区加锁,与lock()区别在于try_lock()不会阻塞线程,lock()会阻塞线程;

    • 如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回true;
    • 如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回false;

    互斥锁被锁定之后可以通过 unlock() 进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。

    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    int g_num = 0;
    mutex g_num_mutex;
    
    void slow_increment(int id)
    {
        for (int i = 0; i < 3; i++)
        {
            //g_num_mutex.lock();
            ++g_num;
            cout << id << "->" << g_num << endl;
            //g_num_mutex.unlock();
            this_thread::sleep_for(chrono::seconds(1));
        }
    }
    
    int main()
    {
        thread t1(slow_increment, 0);
        thread t2(slow_increment, 1);
        t1.join();
        t2.join();
        system("pause");
    }
    
    • 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

    上述代码中执行加锁与注释加锁执行结果对比如下:
    在这里插入图片描述
    try_lock使用:

    void try_lock_test(int id)
    {
        for (int i = 0; i < 3; i++)
        {
            if (g_num_mutex.try_lock())
            {
                ++g_num;
                cout << id << "->" << g_num << endl;
                g_num_mutex.unlock();
            }
            this_thread::sleep_for(chrono::seconds(1));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2. std::lock_guard

    lock_guard 是 C++11 新增的一个模板类,使用这个类,可以简化互斥锁 lock() 和 unlock() 的写法,同时也更安全。

    lock_guard 在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记 unlock() 操作而导致线程死锁。lock_guard 使用了 RAII 技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。
    使用 lock_guard 对上面的例子进行修改,代码如下:

    void slow_increment(int id)
    {
        for (int i = 0; i < 3; ++i) 
        {
            // 使用哨兵锁管理互斥锁
            lock_guard<mutex> lock(g_num_mutex);
            ++g_num;
            cout << id << " => " << g_num << endl;
            this_thread::sleep_for(chrono::seconds(1));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3. std::recursive_mutex

    递归互斥锁 std::recursive_mutex 允许同一线程多次获得互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题,在下面的例子中使用独占非递归互斥量会发生死锁:

    #include 
    #include 
    #include 
    using namespace std;
    
    struct Calculate
    {
        Calculate() : m_i(6) {}
    
        void mul(int x)
        {
            lock_guard<mutex> locker(m_mutex);
            m_i *= x;
        }
    
        void div(int x)
        {
            lock_guard<mutex> locker(m_mutex);
            m_i /= x;
        }
    
        void both(int x, int y)
        {
            lock_guard<mutex> locker(m_mutex);
            mul(x);
            div(y);
        }
    
        int m_i;
        mutex m_mutex;
    };
    
    int main()
    {
        Calculate cal;
        cal.both(6, 3);
        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

    运行结果:
    在这里插入图片描述
    上面的程序中执行了 cal.both(6, 3); 调用之后,程序就会发生死锁,在 both() 中已经对互斥锁加锁了,继续调用 mult() 函数,已经得到互斥锁所有权的线程再次获取这个互斥锁的所有权就会造成死锁(在 C++ 中程序会异常退出,使用 C 库函数会导致这个互斥锁永远无法被解锁,最终阻塞所有的线程)。要解决这个死锁的问题,一个简单的办法就是使用递归互斥锁 std::recursive_mutex,它允许一个线程多次获得互斥锁的所有权。修改之后的代码如下:

    #include 
    #include 
    #include 
    using namespace std;
    
    struct Calculate
    {
        Calculate() : m_i(6) {}
    
        void mul(int x)
        {
            lock_guard<recursive_mutex> locker(m_mutex);
            m_i *= x;
        }
    
        void div(int x)
        {
            lock_guard<recursive_mutex> locker(m_mutex);
            m_i /= x;
        }
    
        void both(int x, int y)
        {
            lock_guard<recursive_mutex> locker(m_mutex);
            mul(x);
            div(y);
        }
    
        int m_i;
        recursive_mutex m_mutex;
    };
    
    int main()
    {
        Calculate cal;
        cal.both(6, 3);
        cout << "cal.m_i = " << cal.m_i << 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

    虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题,但是还是建议少用,主要原因如下:

    使用递归互斥锁的场景往往都是可以简化的,使用递归互斥锁很容易放纵复杂逻辑的产生,从而导致bug的产生
    递归互斥锁比非递归互斥锁效率要低一些。
    递归互斥锁虽然允许同一个线程多次获得同一个互斥锁的所有权,但最大次数并未具体说明,一旦超过一定的次数,就会抛出std::system错误。

    4. std::timed_mutex

    std::timed_mutex 是超时独占互斥锁,主要是在获取互斥锁资源时增加了超时等待功能,因为不知道获取锁资源需要等待多长时间,为了保证不一直等待下去,设置了一个超时时长,超时后线程就可以解除阻塞去做其他事情了。
    std::timed_mutex 比 std::_mutex 多了两个成员函数:try_lock_for() 和 try_lock_until():
    try_lock_for 函数是当线程获取不到互斥锁资源的时候,让线程阻塞一定的时间长度
    try_lock_until 函数是当线程获取不到互斥锁资源的时候,让线程阻塞到某一个指定的时间点
    关于两个函数的返回值:当得到互斥锁的所有权之后,函数会马上解除阻塞,返回 true,如果阻塞的时长用完或者到达指定的时间点之后,函数也会解除阻塞,返回 false
    下面的示例程序中为大家演示了 std::timed_mutex 的使用:

    #include 
    #include 
    #include 
    using namespace std;
    
    timed_mutex g_mutex;
    
    void work()
    {
        chrono::seconds timeout(1);
        while (true)
        {
            // 通过阻塞一定的时长来争取得到互斥锁所有权
            if (g_mutex.try_lock_for(timeout))
            {
                cout << "当前线程ID: " << this_thread::get_id() 
                    << ", 得到互斥锁所有权..." << endl;
                // 模拟处理任务用了一定的时长
                this_thread::sleep_for(chrono::seconds(10));
                // 互斥锁解锁
                g_mutex.unlock();
                break;
            }
            else
            {
                cout << "当前线程ID: " << this_thread::get_id() 
                    << ", 没有得到互斥锁所有权..." << endl;
                // 模拟处理其他任务用了一定的时长
                this_thread::sleep_for(chrono::milliseconds(50));
            }
        }
    }
    
    int main()
    {
        thread t1(work);
        thread t2(work);
    
        t1.join();
        t2.join();
    
        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

    在上面的例子中,通过一个 while 循环不停的去获取超时互斥锁的所有权,如果得不到就阻塞 1 秒钟,1 秒之后如果还是得不到阻塞 50 毫秒,然后再次继续尝试,直到获得互斥锁的所有权,跳出循环体。
    关于递归超时互斥锁 std::recursive_timed_mutex 的使用方式和 std::timed_mutex 是一样的,只不过它可以允许一个线程多次获得互斥锁所有权,而 std::timed_mutex 只允许线程获取一次互斥锁所有权。另外,递归超时互斥锁 std::recursive_timed_mutex 也拥有和 std::recursive_mutex 一样的弊端,不建议频繁使用。

    声明该文章转载自此篇文章,欢迎大家支持关注原作者!

  • 相关阅读:
    【WIFI】【WPS】基础介绍(主要根据sniffer log角度和kernel log去介绍)
    Docker容器镜像
    Retrieve Anything To Augment Large Language Models
    基于uniapp框架的古汉语学习考试系统 微信小程序python+java+node.js+php
    第八章 将对象映射到 XML - 仅映射对象标识符
    大数据 为什么用
    【OpenCV】基于OpenCV/C++实现yolo目标检测
    学习-Java输入输出之随机IO流之向文件中追加内容
    2023年09月 C/C++(六级)真题解析#中国电子学会#全国青少年软件编程等级考试
    Vue 使用 v-bind 动态绑定 CSS 样式
  • 原文地址:https://blog.csdn.net/Snow__Sunny/article/details/127551738