关于c++中条件变量的分析
Hello,各位看官好,本人最近研究了关于各种锁以及条件变量的用法,到此来跟各位分享一下。
一、线程中间存在的同步和互斥的问题
二、lock_guard和unique_lock的区别
三、关于c++中所有锁的用法和区别
四、Condition_variable的用法
五、实例分析
一、线程中间存在的同步和互斥的问题
在操作系统中,多线程一直是一大问题,最大的问题可能就是并发访问时候出现的创建,读取,涂写,删除等操作,导致不可预测的结果。我们这里简单来分析一下。
首先我们来记住一句话,只有在一种情况下我们不需要担心同步和互斥的问题:
多个线程并发处理相同的数据而又不曾同步化,那么唯一安全的情况就是:所有线程只读取数据。
如果不是这种情况会怎么样呢?会导致data race,即是不同线程中的两个互相冲突的动作,其中至少一个不是atomic的,而且无一个动作发生在另一个动作之前。这会导致不可预知的错误。
那么,会导致什么结果呢?我们来看以下可能:
1、写入半途的数据(如果再写入)
2、传递参数时有可能会导致重排顺序
那么针对这个问题我们应该如何处理呢?处理的关键点有两个:1、第一就是我们必须保证其是不可分割的。2、我们必须保证其是有顺序的。也就是我们今天讨论的同步和互斥。
二、lock_guard和unique_lock的区别
我们首先来说互斥,互斥最常用的方法就是lock_guard和unique_lock这两个锁,那么这两个锁究竟有什么区别呢?我们首先来看lock_guard。
while(true) {
std::lock_guard<std::mutex> lock(my_lock);
if (num < 100)
{
//运行条件 num += 1; sum += num;
}
else {
//退出条件 break;
}
}
我们来看这段代码,最奇怪的地方在于他没有上锁和解锁的过程。首先我们要知道,lock_guard是一个类,这个类中一定是有构造和析构函数的,那么lock_guard正式利用了这一点,在构造的时候加锁,在析构的时候解锁。那么构造好说,但是什么时候析构呢?就是在碰到我们第二个括号的时候。
另外就是这段代码还有一点我们要注意,就是在判断条件的时候一定要用while,而不能用if,为什么呢?这是因为在锁中会存在一个虚假唤醒的问题,如果用if,就不会对其进行二次检查,而如果用while就可以完美解决这个问题了。
接下来我们说一说unique_lock,这个锁是lock_guard的升级版本,那么它究竟升级到哪里了呢?就是它可以自由的判断出锁是否添加上,以及是否解除锁。另外,他可以自由的决定是不是一开始就加锁。举例如下。
If (lock()) { // 判断是否加锁成功
}
If (try_lock()) { // 判断是否加锁,如果没有加锁
}
Std::unique_lock<std::mutex> lockM1(m1, std::defer_lock) //初始化的时候不加锁
三、关于c++中各种锁的使用
我们知道,在c++中有各种锁,比如互斥锁,自旋锁,读写锁这三种比较常用,我们刚才介绍了互斥锁,我们简单说下自旋锁,自旋锁和互斥锁的区别在于自旋锁是阻塞锁,互斥锁是非阻塞锁,就是说如果两个线程,线程一用了互斥锁,线程二碰到互斥锁此时会处理别的事情,而碰到自旋锁会导致一直阻塞到那里。而读写锁是针对读写操作的一种特殊锁,它的核心就是一写多读。(只是目前我只用到了互斥锁)
四、条件变量
条件变量更多的用于同步。它的核心就是condition_variable。这里有以下几点需要说明:
1、条件变量一定要和锁一块用,同步的前提是互斥。
2、这里最核心的两个函数,一个是wait函数,另一个是notify函数。
Wait函数:原型为wait(mutex, function) (前者是锁,后者是判断的函数(可以用Lamaba表达式来进行书写))。另外,最重要的一点(如果wait里面的条件不满足,他会阻塞,会挂起。如果满足正常执行)。
Notify函数,在所有东西弄完了以后,使用notify函数通知其他的可以用了。
五、实例分析
#include "mainwindow.h"
#include <QApplication>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <deque>
#include <iostream>
#define MAX_THREAD 3
#define MAX_NUM 30
std::mutex g_pcMutex;
std::condition_variable g_pcCondition;
std::deque<int> g_package;
int g_nextIndex = 0;
void productorFunction(int id)
{
// add lock
while (1) {
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::unique_lock<std::mutex> l1(g_pcMutex);
// wait
g_pcCondition.wait(l1, [](){return g_package.size() <= MAX_NUM;});
// exec
g_nextIndex++;
g_package.push_back(g_nextIndex);
std::cout<<"productor thread id is:"<<id<<"g_nextIndex is:"<<g_nextIndex<<std::endl;
std::cout<<"productor deque size is:"<<g_package.size()<<std::endl;
// notify
g_pcCondition.notify_all();
}
}
void consumerFunction(int id)
{
while(1) {
// create mutex
// std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::unique_lock<std::mutex> l2(g_pcMutex);
// wait
g_pcCondition.wait(l2, [](){std::cout<<"consumer wait"<<std::endl; return g_package.size() <= 2;});
// g_pcConditon--
g_nextIndex--;
// deque pushfront
g_package.pop_back();
std::cout<<"consumer thread id is:"<<id<<"g_nextIndex is:"<<g_nextIndex<<std::endl;
std::cout<<"consumer deque size is:"<<g_package.size()<<std::endl;
// notify
g_pcCondition.notify_all();
}
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
std::thread consumerThread[MAX_THREAD];
std::thread productorThread[MAX_THREAD];
for (int i=0;i<MAX_THREAD;i++) {
consumerThread[i] = std::thread(consumerFunction, i);
}
for(int i=0;i<MAX_THREAD;i++) {
productorThread[i] = std::thread(productorFunction, i);
}
for (int i=0;i<MAX_THREAD;i++) {
consumerThread[i].join();
}
for (int i=0;i<MAX_THREAD;i++) {
productorThread[i].join();
}
MainWindow w;
w.show();
return a.exec();
}
这是一个消费者和管理者的典型问题。我这里要说的只有一个:g_pcCondition.wait(l2, [](){std::cout<<"consumer wait"<<std::endl; return g_package.size() <= 2;});
这句话的意思就是小于2执行,大于2不执行,这就保证里面最少两个值。
另外,针对线程的问题一定要删除重新编译,否则会出现莫名奇妙的问题。