• C++多线程编程(互斥锁、条件变量)


    互斥锁std::mutex

      C++中常见的cout是一个共享资源,如果在多个线程同时执行cout,会发现很奇怪的问题,解决办法就是要对cout这个共享资源进行保护。

      在C++中,可以使用互斥锁std::mutex进行资源保护,头文件是#include <mutex>,共有两种操作:锁定(lock)与解锁(unlock)。

      将cout重新封装成一个线程安全的函数

    #include <iostream>
    #include <thread>
    #include <string>
    #include <mutex>
    using namespace std;
    
    std::mutex mu;
    // 使用锁保护
    void shared_print(string msg, int id) {
        mu.lock(); // 上锁
        cout << msg << id << endl;
        mu.unlock(); // 解锁
    }
    
    void function_1() {
        for(int i=0; i>-100; i--)
            shared_print(string("From t1: "), i);
    }
    
    int main()
    {
        std::thread t1(function_1);
    
        for(int i=0; i<100; i++)
            shared_print(string("From main: "), i);
    
        t1.join();
        return 0;
    }

      

    互斥锁std::lock_guard

      但是,如果mu.lock()mu.unlock()之间的语句发生了异常,unlock()语句没有机会执行!导致导致mu一直处于锁着的状态,其他使用shared_print()函数的线程就会阻塞。解决这个问题也很简单,使用c++中常见的RAII技术,即获取资源即初始化(Resource Acquisition Is Initialization)技术,这是C++中管理资源的常用方式。简单的说就是在类的构造函数中创建资源,在析构函数中释放资源,因为就算发生了异常,C++也能保证类的析构函数能够执行。C++库提供了std::lock_guard类模板,使用方法如下:

    void shared_print(string msg, int id) {
        //构造时上锁,析构时释放锁
        std::lock_guard<std::mutex> guard(mu);
        cout << msg << id << endl;
    }
      如果你将某个mutex上锁了,却一直不释放,另一个线程访问该锁保护的资源的时候,就会发生死锁,这种情况下使用lock_guard可以保证析构的时候能够释放锁,然而,当一个操作需要使用两个互斥元的时候,仅仅使用lock_guard并不能保证不会发生死锁
    1. 可以比较mutex的地址,每次都先锁地址小的
    if(&_mu < &_mu2){
        _mu.lock();
        _mu2.lock();
    }
    else {
        _mu2.lock();
        _mu.lock();
    }

    2. 使用层次锁,将互斥锁包装一下,给锁定义一个层次的属性,每次按层次由高到低的顺序上锁

      C++标准库中提供了std::lock()函数,能够保证将多个互斥锁同时上锁

    std::lock(_mu, _mu2);

      这两种办法其实都是严格规定上锁顺序,只不过实现方式不同。同时,lock_guard也需要做修改,因为互斥锁已经被上锁了,那么lock_guard构造的时候不应该上锁,只是需要在析构的时候释放锁就行了,使用std::adopt_lock表示无需上锁

    std::lock_guard<std::mutex> guard(_mu2, std::adopt_lock);
    std::lock_guard<std::mutex> guard2(_mu, std::adopt_lock);

    std::unique_lock

      互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域。lock_guard只能保证在析构的时候执行解锁操作,l没有提供加锁和解锁的接口。

      unique_lock提供了lock()unlock()接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁,可以使用std::defer_lock设置初始化的时候不进行默认的上锁操作

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

    条件变量std::condition_variable

      C++11中提供了#include <condition_variable>头文件,其中的std::condition_variable可以和std::mutex结合一起使用

      wait(),可以让线程陷入休眠状态,wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作;

      notify_one()唤醒处于wait中的其中一个条件变量(可能当时有很多条件变量都处于wait状态);

      notify_all(),可以同时唤醒所有处于wait状态的条件变量#include <iostream#include <dequ#include <thread>

    #include <mutex>
    #include <condition_variable>
    
    std::deque<int> q;
    std::mutex mu;
    std::condition_variable cond;
    
    //生产者
    void function_1() {
        int count = 10;
        while (count > 0) {
            std::unique_lock<std::mutex> locker(mu);//上锁
            q.push_front(count);//往队头放数据
            locker.unlock();//解锁
            cond.notify_one();  // 通知一个等待的线程
            std::this_thread::sleep_for(std::chrono::seconds(1));
            count--;
        }
    }
    
    //消费者
    void function_2() {
        int data = 0;
        while ( data != 1) {
            std::unique_lock<std::mutex> locker(mu);//上锁
            while(q.empty())
                cond.wait(locker); //解锁并等待通知
            data = q.back();//取队尾数据
            q.pop_back();//删除队尾数据
            locker.unlock();//解锁
            std::cout << data << std::endl;
        }
    }
    
    int main() {
        std::thread t1(function_1);//生产者
        std::thread t2(function_2);//消费者
        t1.join();
        t2.join();
        return 0;
    }
      在function_2中,在判断队列是否为空的时候,使用的是while(q.empty()),而不是if(q.empty()),这是因为wait()从阻塞到返回,不一定就是由于notify_one()函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒,如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()阻塞。
     

    原文链接:https://www.jianshu.com/p/c1dfa1d40f53
  • 相关阅读:
    LeetCode
    已知二叉树的先序遍历和中序遍历序列求后序遍历序列
    LeetCode
    LeetCode
    LeetCode
    LeetCode
    LeetCode
    TCP协议的基本规则和在Java中的使用
    Java中UDP协议的基本原理和简单用法
    LeetCode
  • 原文地址:https://www.cnblogs.com/yongjin-hou/p/14529320.html
Copyright © 2020-2023  润新知