• 条件变量condition_variable实现线程同步


    一.condition_variable使用

    1.1 condition_variable条件变量介绍

      在C++11中,我们可以使用条件变量(condition_variable)实现多个线程间的同步操作;当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。
      其相关的成员函数如下:
    在这里插入图片描述
    条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

    • 一个线程因等待"条件变量的条件成立"而挂起;
    • 另外一个线程使"条件成立",给出信号,从而唤醒被等待的线程。

      为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起;通常情况下这个锁是std::mutex,并且管理这个锁 只能是 std::unique_lock RAII模板类。

    上面提到的两个步骤,分别是使用以下两个方法实现:

    • 等待条件成立使用的是condition_variable类成员wait 、wait_for 或 wait_until。
    • 给出信号使用的是condition_variable类成员notify_one或者notify_all函数。

    1.2 condition_variable成员函数使用

    1.2.1 wait()成员函数

    (1)void wait( std::unique_lock& lock )
      先unlock释放之前获得的mutex,然后阻塞当前的执行线程。把当前线程添加到等待线程列表中,该线程会持续 block 直到被 notify_all() 或 notify_one() 唤醒。被唤醒后,该thread会重新获取mutex,获取到mutex后执行后面的动作

    示例:

    #include         
    #include 
    #include 
    #include 
    
    std::mutex               g_mtx_;
    std::condition_variable  g_cond_;
    
    using namespace std;
    
    void test_wait() {
        //定义一个unique_lock互斥锁变量lock
        std::unique_lock<std::mutex> lock(g_mtx_);
        //条件变量阻塞等待
        g_cond_.wait(lock);
    
        printf("---------I am test_wait---------\n");
    }
    
    int main() {
        printf("-------------test_wait-------------\n");
        std::thread t1(test_wait);    //创建子线程t1
        printf("------------sleep start------------\n");
        printf("这里延时了2s\n");
        std::this_thread::sleep_for(2000ms);  //主线程阻塞2s,此时子线程在阻塞wait
        printf("-------------sleep end-------------\n");
        {
            //在发送通知给到子线程条件变量之前,需要给mutex互斥锁上锁
            std::unique_lock<std::mutex> lock(g_mtx_);
        }
        //通知一个子线程
        g_cond_.notify_one();
        //回收子线程t1
        t1.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

    编译 && 运行:

    g++ mian.cpp -lpthread
    ./a.out

    执行结果:
    在这里插入图片描述
    (2)template< class Predicate > void wait( std::unique_lock& lock, Predicate pred ); //Predicate 谓词函数,可以普通函数或者lambda表达式

      wait 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足某谓词。该重载设置了第二个参数 Predicate, 只有当pred为false时,wait才会阻塞当前线程。等同于下面:

    template<typename _Predicate>
    void wait(unique_lock<mutex>& __lock, _Predicate __pred)
    {
        while (!__pred())
            wait(__lock);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

      该情况下,线程被唤醒后,先重新判断pred的值如果pred为false,则会释放mutex并重新阻塞在wait。因此,该mutex必须有pred的权限。该重载消除了意外唤醒的影响。总的来说,如果想要唤醒条件变量的wait等待,首先,需要_Predicate lambda表达式执行为true;然后,需要获得notify

    示例:

    #include         
    #include 
    #include 
    #include 
    #include 
    #include 
    
    std::mutex               g_mtx_;     //定义互斥信号量
    std::condition_variable  g_cond_;    //定义条件变量
    std::atomic<bool>        g_pred_;    //定义原子变量
    
    using namespace std;
    
    void test_wait_pred() {
        //定义互斥信号量lock
        std::unique_lock<std::mutex> lock(g_mtx_);
        //条件变量wait等待notify,首先阻塞等待g_pred_.load() == true,然后再wait
        g_cond_.wait(lock, []{ 
            std::cout<<"_Predicate lambda执行!!!"<<std::endl;
            return g_pred_.load(); 
            }); //等同于下面的表达
        /*
            {
            while (!lambda())
                wait(__lock);
            }
        */
        printf("---------I am test_wait_pred---------\n");
    }
    
    int main() {
        printf("----------test_wait_pred-----------\n");
        std::thread t1(test_wait_pred);  //创建一个子线程t1
        printf("----------awaken first-------------\n");
        {
            //首先给mutex互斥信号量上锁
            std::unique_lock<std::mutex> lock(g_mtx_);
        }
        //给子线程的条件变量g_cond_发通知,此时并不会触发子线程推出wait,因为__pred == false
        //所以第一次唤醒会失败!
        g_cond_.notify_one();
        this_thread::sleep_for(2000ms);
        printf("此处延时了2s\n");
        
        printf("----------awaken second------------\n");
        {
            //首先给mutex互斥信号量上锁
            std::unique_lock<std::mutex> lock(g_mtx_);
            //原子变量置为true,激活__pred == true
            g_pred_.store(true);
        }
        //给子线程的条件变量g_cond_发通知
        g_cond_.notify_one();
        //回收子线程
        t1.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
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    编译 && 运行:

    g++ mian.cpp -lpthread
    ./a.out

    执行结果:
    在这里插入图片描述
      分析
      这里在主线程中对子线程的cv.wait()尝试唤醒了两次,第一次是直接发送notify_one();而并没有使得_Predicate的lambda表法式返回true,所以awaken first只是执行了一下_Predicate的lambda表法式,而并没有唤醒子线程而使得子线程推出阻塞;
      第二次是先激活__pred == true,使得_Predicate的lambda表法式返回为true,然后再发送notify_one();所以第二次成功的唤醒了子线程。
      另外,可以看出,每次由主线程发送的notify_one(),都会使得_Predicate的lambda表法式执行一次,不同的是要根据lambda表法式的输出结果来决定要不要使得cv.wait()唤醒!

    1.2.2 wait_for()成员函数

    函数声明如下:

    1template< class Rep, class Period >
    std::cv_status wait_for( std::unique_lock<std::mutex>& lock,
                             const std::chrono::duration<Rep, Period>& rel_time);2template< class Rep, class Period, class Predicate >
    bool wait_for( std::unique_lock<std::mutex>& lock,
                   const std::chrono::duration<Rep, Period>& rel_time,
                   Predicate pred);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    wait_for 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,或者超时返回。

    返回值说明:

    (1)若经过 rel_time 所指定的关联时限则为 std::cv_status::timeout ,否则为 std::cv_status::no_timeout 。

    (2)若经过 rel_time 时限后谓词 pred 仍求值为 false 则为 false ,否则为 true 。

    以上两个类型的wait函数都在会阻塞时,自动释放锁权限,即调用unique_lock的成员函数unlock(),以便其他线程能有机会获得锁。这就是条件变量只能和unique_lock一起使用的原因,否则当前线程一直占有锁,线程被阻塞。

    1.2.3 notify_all/notify_one成员函数

    notify函数声明如下:

    //若任何线程在 *this 上等待,则调用 notify_one 会解阻塞(唤醒)等待线程之一。
    void notify_one() noexcept;
    //若任何线程在 *this 上等待,则解阻塞(唤醒)全部等待线程。
    void notify_all() noexcept;
    
    • 1
    • 2
    • 3
    • 4

    虚假唤醒:
      在正常情况下,wait类型函数返回时要不是因为被唤醒,要不是因为超时才返回,但是在实际中发现,因此操作系统的原因,wait类型在不满足条件时,它也会返回,这就导致了虚假唤醒。因此,我们一般都是使用带有谓词参数的wait函数,因为这种(xxx, Predicate pred )类型的函数等价于:

    while (!pred()) //while循环,解决了虚假唤醒的问题
    {
        wait(lock);
    }
    
    • 1
    • 2
    • 3
    • 4

    原因说明如下:

    假设系统不存在虚假唤醒的时,代码形式如下:

    if (不满足xxx条件)
    {
        //没有虚假唤醒,wait函数可以一直等待,直到被唤醒或者超时,没有问题。
        //但实际中却存在虚假唤醒,导致假设不成立,wait不会继续等待,跳出if语句,
        //提前执行其他代码,流程异常
        wait();  
    }
    //其他代码
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    正确的使用方式,使用while语句解决:

    while (!(xxx条件) )
    {
        //虚假唤醒发生,由于while循环,再次检查条件是否满足,
        //否则继续等待,解决虚假唤醒
        wait();  
    }
    //其他代码
    ....
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    1.3 条件变量的使用场景示例:生产者消费者问题

    在这里,我们使用条件变量,解决生产者-消费者问题,该问题主要描述如下:

    生产者-消费者问题,也称有限缓冲问题,是一个多进程/线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程/线程——即所谓的“生产者”和“消费者”,在实际运行时会发生的问题。

    生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

    要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。

    同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。

    生产者-消费者代码如下:

    #include         
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    std::mutex g_cvMutex;           //定义条件变量使用的mutex
    std::condition_variable g_cv;   //定义一个条件变量
    //缓存deque区
    std::deque<int> g_data_deque;
    //缓存区最大数目
    const int  MAX_NUM = 30;
    //数据
    int g_next_index = 0;
    //生产者,消费者线程个数:3
    const int PRODUCER_THREAD_NUM  = 3;
    const int CONSUMER_THREAD_NUM = 3;
    
    //生产者线程
    void  producer_thread(int thread_id)
    {
    	 while (true)
    	 {
             //生产者线程休眠500ms,而消费者休眠550ms,所以生产者生产的速度要比消费者快
    	     std::this_thread::sleep_for(std::chrono::milliseconds(500));
    	     //加锁
    	     std::unique_lock <std::mutex> lk(g_cvMutex);
    	     //当队列未满时,继续添加数据
    	     g_cv.wait(lk, [](){ return g_data_deque.size() <= MAX_NUM; });
    	     g_next_index++;  //数据值+1
    	     g_data_deque.push_back(g_next_index); //将增加的数据push到deque中
    	     std::cout << "producer_thread: " << thread_id << "          producer data: " << g_next_index;
    	     std::cout << "         queue size: " << g_data_deque.size() << std::endl;
    	     //唤醒其他线程 
    	     g_cv.notify_all();
    	     //自动释放锁
    	 }
    }
    void  consumer_thread(int thread_id)
    {
        while (true)
        {
            //生产者线程休眠500ms,而消费者休眠550ms,所以生产者生产的速度要比消费者快
            std::this_thread::sleep_for(std::chrono::milliseconds(550));
            //加锁
            std::unique_lock <std::mutex> lk(g_cvMutex);
            //检测条件是否达成
            g_cv.wait( lk,   []{ return !g_data_deque.empty(); });
            //互斥操作,消息数据
            int data = g_data_deque.front();  //从deque队列中获取头部数据
            g_data_deque.pop_front();         //deque头部数据出队列
            std::cout << "\tconsumer_thread: " << thread_id << "          consumer data: ";
            std::cout << data << "         deque size: " << g_data_deque.size() << std::endl;
            //唤醒其他线程
            g_cv.notify_all();
            //自动释放锁
        }
    }
    int main()
    {
        std::thread arrRroducerThread[PRODUCER_THREAD_NUM];   //定义生产者线程函数队列(3)
        std::thread arrConsumerThread[CONSUMER_THREAD_NUM];   //定义消费者线程函数队列(3)
        for (int i = 0; i < PRODUCER_THREAD_NUM; i++)
        {
            arrRroducerThread[i] = std::thread(producer_thread, i);  //创建3个生产者线程
        }
        for (int i = 0; i < CONSUMER_THREAD_NUM; i++)
        {
            arrConsumerThread[i] = std::thread(consumer_thread, i);  //创建3个消费者线程
        }
        for (int i = 0; i < PRODUCER_THREAD_NUM; i++)
        {
            arrRroducerThread[i].join();   //回收3个生产者线程
        }
        for (int i = 0; i < CONSUMER_THREAD_NUM; i++)
        {
            arrConsumerThread[i].join();   //回收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
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    编译 && 运行:

    g++ mian.cpp -lpthread
    ./a.out

    执行结果:
    在这里插入图片描述
      可以看出,生成者和消费者之间通过条件变量g_cv实现了互锁,在生产者线程生产数据的时候,占用了mutex互斥锁,此时消费者线程只能阻塞等待,当生产者完成数据生产之后,会释放mutex互斥锁,并且发送notify通知,告诉消费者可以去deque中取数据了。
      消费者线程获得锁之后,会将mutex上锁以确保生产者线程在消费者读数据的过程中不会向其中写数据。消费者从deque中读取数据,然后通知生产商者线程(线程同步)并释放锁。
      上述的两个线程的cv.wait()都采用了_Predicate lambda的方式,也就是唤醒wait之前必须执行lambda表达式并且返回值为true,上述的lambda表达式是以deque中的数据量作为出发条件,通常可以在两个线程中设置原子变量实现lambda的判断。

    二.基于condition_variable条件变量实现线程同步

      下面程序实现了通过子线程获取处理完的数据,整体的思路与上述的生产者-消费者相似,只是将子线程封装在一个类中;在主线程中首先触发_Predicate lambda == true,然后notify通知子线程进行数据处理,并将处理后的数据放在类的buffer中。然后在主线程中wait_async_response(通过原子变量判断)。这里通过类的封装实现请求buffer和响应buffer的预置,在数据请求和数据返回的数据是存储在对象的成员变量中,这样的好处是:在主线程中可以不着急获取数据,而是在需要的时候从子线程对象中获取,实现了主线程buffer和子线程类buffer的双Buffer同步。

    程序源码:

    #include         
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    class threadtest
    {
    public:
        // 外部传入的数据处理函数(可以向远程server完成数据请求)
        using request_func_type = std::function<std::string(const std::string&)>;
    
        threadtest(request_func_type request_func) 
            : request_done_(true)    //请求结束标志初始设置为true
            , thread_done_(false)    //线程结束标志初始设置为fase
        {
            auto thread_func = [this, request_func] {
                std::cout<<"子线程函数执行"<<std::endl;
                while (wait_async_request()) {
                    try {
                        std::cout<<"子线程解锁完成"<<std::endl;
                        auto response = request_func(request_data_);  //调用外部传入的函数对request数据进行处理,获得response数据
                        async_response(std::move(response));          //将response数据进行处理
                    }
                    catch (std::exception& ex) {
                        std::cout << "[async_response_buffer] request_func exception:" << ex.what() <<std::endl;
                        async_response(std::string());  // 将类的response buffer清空
                    }
                }
                std::cout<<"子线程退出执行"<<std::endl;
            };
            //! 开启一个一步处理的子线程,并执行该子线程的函数thread_func
            async_thread_ = std::move(std::thread(thread_func));
        }
        ~threadtest()
        {
            std::cout<<"threadtest析构函数执行,线程结束标志置为true"<<std::endl;
            thread_done_ = true;  //1.线程结束标志置为true,满足_Predicate lambda == true
            {  
                //2.通知子线程结束运行
                std::lock_guard<std::mutex> lg(mutex_);
                cv_.notify_one();
            }
            // 等待子线程执行完成,对子线程进行回收
            if (async_thread_.joinable()) {
                async_thread_.join();
            }
        }
    
    /*
     * 注意:该public下的两个函数均由外部调用(main函数),首先调用async_request发起数据请求;
     * 然后,通过调用wait_async_response函数获取处理好的数据(wait_async_response函数中会等待数据在子线程中处理完成,通过request_done_标志判断)
    */
    public:  
        /**
         * @brief  通知子线中的while解除阻塞,开始执行request_func函数对传入的数据进行处理
         * @note   该函数由外部调用,作为启动子线程工作的起点***
         * @param[in]   data  传入的请求数据
         */
        void async_request(string&& data)
        {
            request_done_.store(false); //1.满足_Predicate lambda == true
            {
                // 2.通知子线程的while (wait_async_request())解除阻塞,开始执行
                std::lock_guard<std::mutex> lg(mutex_);
                request_data_ = data;
                cv_.notify_one();   //通知一个等待的线程,notify_all()为通知所有等待的线程
            }
        }
        /**
         * @brief   Wait for async response data store done
         * @note   该函数由外部调用,用来判断数据处理已经完成,并且获得处理后的数据
         * @pre     after @a async_request() called
         * @post    @a response_data_ will be moved
         * @return  Return the async response data @a response_data_
         */
        string&& wait_async_response()
        {
            using namespace std;
            //子线程数据处理过程中request_done_=false,处理结束后request_done_=true,也就跳出while循环,结束该函数
            while (!request_done_.load()) {
                this_thread::sleep_for(10us);
            }
            return std::move(response_data_);
        }
    
    private:
        /**
         * @brief   一直等待直到收到一步通知或线程结束
         * @return  如果线程结束了那么就返回false,使得子线程退出执行
         */
        bool wait_async_request()
        {
            std::unique_lock<std::mutex> ul(mutex_);
            std::cout<<"子线程等待解锁...."<<std::endl;
            //先获得mutex_,然后阻塞当前线程,把当前线程添加到等待线程列表,
            //该线程会持续block直到_Predicate lambda == true并且被 notify_all() 或 notify_one() 唤醒。
            //被唤醒后,该thread会重新获取mutex,获取到mutex后执行后面的动作。
            cv_.wait(ul, [this] { 
                std::cout<<"_Predicate lambda执行,返回值为:"<<(thread_done_ || !request_done_.load())<<std::endl;
                return thread_done_ || !request_done_.load(); 
                });
            return !thread_done_;
        }
        /**
         * @brief   对子线程中获得数据进行处理(存放在类buffer中),处理结束后将request_done_置为true
         * @param[in]   data  子线程处理后的数据
         */
        void async_response(std::string&& data)
        {
            response_data_ = data;
            request_done_.store(true);
        }
    
    private:
        /* 用来通知子线程执行请求(解除锁定) */
        std::mutex              mutex_;
        std::condition_variable cv_;
        /*! 表示子线程中数据的请求、处理过程完成,用来保护request_data_和response_data_ */
        std::atomic<bool> request_done_;
        /* 定义的子线程名和线程执行完成标志 */
        bool        thread_done_;   //线程结束标志,使得子线程退出while循环,子线程执行结束
        std::thread async_thread_;
        /* 用来存放该类请求数据和处理后数据 */
        std::string  request_data_;    
        std::string  response_data_; 
    };
    
    // 由主函数传入的数据处理函数
    std::string func(const std::string& in_data)
    {
        return in_data+"-disposed!";
    }
    int main(int argc, char** argv)
    {
        string request_data1 = "abcdef";
        string request_data2 = "123456";
    
        string response_data;
        threadtest thread(func);                                 //定义一个threadtest对象为thread,构造参数为func
        
        std::this_thread::sleep_for(2000ms);
        std::cout<<std::endl<<"此处延时了2s"<<std::endl;
        std::cout<<"-------------first request----------------"<<std::endl;
        thread.async_request(std::move(request_data1));           //1.首先发起数据处理请求(子线程解锁)
        response_data = std::move(thread.wait_async_response()); //2.其次,数据处理结束后,获取处理后的数据
        std::cout<<"received data1: "<<response_data<<std::endl;
    
        std::cout<<"--------------seconed request---------------"<<std::endl;
        thread.async_request(std::move(request_data2));           //1.首先发起数据处理请求(子线程解锁)
        response_data = std::move(thread.wait_async_response()); //2.其次,数据处理结束后,获取处理后的数据
        std::cout<<"received data2: "<<response_data<<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
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158

    编译 && 运行:

    g++ mian.cpp -lpthread
    ./a.out

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

  • 相关阅读:
    数字图像处理 --- 图像的HighBit与LowBit
    算法通过村第十二关青铜挑战——不简单的字符串转换问题
    c++使用ifstream和ofstream报错:不允许使用不完整的类型
    30天Python入门(第二十二天:Python爬虫基础)
    运动酒店,如何“奇袭”文旅产业精准蓝海赛道——缤跃酒店
    Jensen不等式(琴生不等式)
    QT-Linux生成错误日志dump
    MindSpore社区与北大BIOPIC联合发布蛋白质多序列比对开源数据集
    Ansible如何使用lookup插件模板化外部数据
    4000 多字学懂弄通 js 中 this 指向问题,顺便手写实现 call、apply 和 bind
  • 原文地址:https://blog.csdn.net/weixin_42700740/article/details/126226745