C++多线程学习(三)——线程同步之条件变量

前面学习了互斥量似乎我们就可以多线程编程了,多线程也不过如此嘛。然而我们上手coding,用多线程来结局我们实际需求就会发现,似乎多线程也不是很好用。因为我们实际对于多线程的需求,往往线程都是while循环让其不断轮询执行,而不是简单的顺序命令执行完就结束线程了。让我们看个具体的例子深入感受下其中的痛点。

//cond_var1.cpp用互斥锁实现一个生产者消费者模型

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>

std::deque<int> q;						//双端队列标准容器全局变量
std::mutex mu;							//互斥锁全局变量
//生产者,往队列放入数据
void function_1() {
    int count = 10;
    while (count > 0) {
        std::unique_lock<std::mutex> locker(mu);
        q.push_front(count);			//数据入队锁保护
        locker.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));		//延时1秒
        count--;
    }
}
//消费者,从队列提取数据
void function_2() {
    int data = 0;
    int f2Count = 0;
    while ( data != 1) {
        f2Count++;
        std::unique_lock<std::mutex> locker(mu);
        if (!q.empty()) {			//判断队列是否为空
            data = q.back();
            q.pop_back();			//数据出队锁保护
            locker.unlock();
            std::cout << "t2 got a value from t1: " << data << std::endl;
            std::cout << "t2 run times " << f2Count << std::endl;
        } else {
            locker.unlock();
        }
    }
}

int main() {
    std::thread t1(function_1);
    std::thread t2(function_2);
    t1.join();
    t2.join();

    getchar();
    return 0;
}

功能很简单,模拟最常见的收发多线程需求,压数据还好,1s往里面写一个数据,但是接收线程啥时候知道你啥时候有数据了呢?function_2线程就只能不断轮询了,看下执行结果,短短10s,轮询了3亿多次,cpu负荷拉满,我们生产要是写这种代码,老板分分钟鱿鱼套餐走起。

 一个简单的功能就把我们cpu占满了,那如何解决呢,聪明的同学已经知道了,定时轮询,cpu的轮询频率是很夸张的,现在好一点的都有几GHZ,那我们不依赖cpu轮询频率,增加线程休眠时间,别这么快,遭不住了,其实我看到的很多小公司对性能要求不高的程序确实都这么干的,虽简单,但有效。

上代码

//cond_var1.cpp用互斥锁实现一个生产者消费者模型

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>

std::deque<int> q;						//双端队列标准容器全局变量
std::mutex mu;							//互斥锁全局变量
//生产者,往队列放入数据
void function_1() {
    int count = 10;
    while (count > 0) {
        std::unique_lock<std::mutex> locker(mu);
        q.push_front(count);			//数据入队锁保护
        locker.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));		//延时1秒
        count--;
    }
}
//消费者,从队列提取数据
void function_2() {
    int data = 0;
    int f2Count = 0;
    while ( data != 1) {
        f2Count++;
        std::unique_lock<std::mutex> locker(mu);
        if (!q.empty()) {			//判断队列是否为空
            data = q.back();
            q.pop_back();			//数据出队锁保护
            locker.unlock();
            std::cout << "t2 got a value from t1: " << data << std::endl;
            std::cout << "t2 run times " << f2Count << std::endl;
        } else {
            locker.unlock();
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));		//延时50毫秒
    }
}

int main() {
    std::thread t1(function_1);
    std::thread t2(function_2);
    t1.join();
    t2.join();

    getchar();
    return 0;
}

执行结果

 频率降到180了,对于绝大多数生产环境已经算是能接受了,但是也显而易见的,他的反应时间已经不是前面的几千万分之一秒了,而升高到了50ms,50ms似乎不长,但是对于一些对时间高敏感的fps游戏等需求呢?降低休眠时间?可行,但是cpu负荷增加,这时候我们就需要一个东西来满足休眠一个线程,直到符合某个条件时再继续往下执行,这样就不用再左右为难了。

C++标准库在< condition_variable >中提供了条件变量,借由它,一个线程可以唤醒一个或多个其他等待中的线程。原则上,条件变量的运作如下:

  • 你必须同时包含< mutex >和< condition_variable >,并声明一个mutex和一个condition_variable变量;
  • 那个通知“条件已满足”的线程(或多个线程之一)必须调用notify_one()或notify_all(),以便条件满足时唤醒处于等待中的一个条件变量;
  • 那个等待"条件被满足"的线程必须调用wait(),可以让线程在条件未被满足时陷入休眠状态,当接收到通知时被唤醒去处理相应的任务;

 上代码

//cond_var2.cpp用条件变量解决轮询间隔难题

#include <iostream>
#include <deque>
#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));		//延时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 << "t2 got a value from t1: " << data << std::endl;
    }
}

int main() {
    std::thread t1(function_1);
    std::thread t2(function_2);
    t1.join();
    t2.join();

    getchar();
    return 0;
}




执行结果

 发送线程通知接收线程的时间损耗是多久呢,我们打印下收发时间

//cond_var2.cpp用条件变量解决轮询间隔难题
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <time.h>



std::deque<int> q;						//双端队列标准容器全局变量
std::mutex mu;							//互斥锁全局变量
std::condition_variable cond;           //全局条件变量

//毫秒级时间戳
std::time_t getTimeStamp()
{
    std::chrono::time_point<std::chrono::system_clock,std::chrono::milliseconds> tp = std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::system_clock::now());//获取当前时间点
    std::time_t timestamp =  tp.time_since_epoch().count(); //计算距离1970-1-1,00:00的时间长度
    return timestamp;
}

//生产者,往队列放入数据
void function_1() {
    int count = 10;
    while (count > 0) {
        std::unique_lock<std::mutex> locker(mu);
        q.push_front(count);			//数据入队锁保护
        time_t timestamp = getTimeStamp();
        locker.unlock();
        
        cond.notify_one();              // 向一个等待线程发出“条件已满足”的通知
        std::cout << "ptime:" << timestamp << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));		//延时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();
        time_t timestamp = getTimeStamp();
        q.pop_back();			//数据出队锁保护
        locker.unlock();
        std::cout << "rtime:" << timestamp << std::endl;
        std::cout << "t2 got a value from t1: " << data << std::endl;
    }
}

int main() {
    std::thread t1(function_1);
    std::thread t2(function_2);
    t1.join();
    t2.join();

    getchar();
    return 0;
}


 看运行结果,我们发现算上unlock(),通知以及切换线程、获取时间戳的总损耗控制再1ms以内,这完全可接受了。

 这样看来条件向量算是很优秀的控制切换方案了,但是使用上我是有个疑问的,function_2在调用cond.wait()之前是把mu锁住的,现在线程又阻塞在这里休眠没有unlock(),那function_1不是会死锁吗?为了模拟这种情况,我写了个例子,线程function_2,lock()后休眠1000s,实际function_1确实阻塞住了。

//cond_var1.cpp用互斥锁实现一个生产者消费者模型

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>

std::deque<int> q;						//双端队列标准容器全局变量
std::mutex mu;							//互斥锁全局变量
//生产者,往队列放入数据
void function_1() {
    int count = 10;
    while (count > 0) {
        std::unique_lock<std::mutex> locker(mu);
        q.push_front(count);			//数据入队锁保护
        locker.unlock();
        std::cout << "t1 push a value to q: " << count << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));		//延时1秒
        count--;
    }
}
//消费者,从队列提取数据
void function_2() {
    int data = 0;
    int f2Count = 0;
    while ( data != 1) {
        f2Count++;
        std::unique_lock<std::mutex> locker(mu);
        std::this_thread::sleep_for(std::chrono::seconds(1000));		//休眠1000s
        if (!q.empty()) {			//判断队列是否为空
            data = q.back();
            q.pop_back();			//数据出队锁保护
            locker.unlock();
            std::cout << "t2 got a value from t1: " << data << std::endl;
            std::cout << "t2 run times " << f2Count << std::endl;
        } else {
            locker.unlock();
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));		//延时50毫秒
    }
}

int main() {
    std::thread t1(function_1);
    std::thread t2(function_2);
    t1.join();
    t2.join();

    getchar();
    return 0;
}

 运行结果

 而前面的例子,我们可以看到cond.wait导致function_2阻塞后,并不影响function_1的执行,这里就可以知道,wait后的mutex锁,是可以被再次lock()往下执行的,而且wait被唤起往下执行的时候,是需要unlock掉wait前的锁定。实际上wait被调用时该mutex必须被unique_lock锁定,否则会发生不明确的行为。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值