目录
写在前面
线程同步方式很多种,但本人觉得工作中用得较多的就是条件变量,至于条件变量概念什么的,本文只会简单提及,因为官网的才是权威请移步cpp官方文档。我也不会贴什么官方文档的东西糊弄,本人也反感。
正文开始(不想看文字的可以直接跳到代码)
我们学一个知识点时,不该只是会用,还要知道其原理(当然有些原理太难,就算了,比如条件变量),但也要知道为什么要这样用,对知识点发出疑问,才是真正理解(正所谓知其然而知其所以然)。
1.条件变量是什么?干嘛的?
概念:条件变量是一种用于线程间通信的同步机制,主要用于线程间的“等待-通知”模型。它配合互斥锁(std::mutex
)使用,使一个线程可以等待某个条件成立,而另一个线程在条件满足后通知其继续执行。
简单的说,就是B线程一直在等着,没有继续往下执行,然后A线程通过条件变量让B不用等了,可以往下执行了。
2.为什么要用条件变量?
通过概念,我们知道条件变量是等待-通知,那我们用其他线程同步方式可以不呢,可以的,兄弟,比如互斥锁(std::mutex), 它也可以让某个线程等在那里,直到条件成立,伪代码
while(true) {
if (条件成立)
break;
}
那这里是不是一直在占着CPU资源,对吧。那condition_variable就避免了这种让CPU空转的问题。conditon_variable是怎么做的呢,它通过操作系统机制挂起线程,等条件满足时唤醒,线程就不会一直在那儿空转了,当然背后的机制就不是我们探讨的啦。
3.如何使用?
c++是面向对象的语言,那这个condition_varible,肯定是一个类(好像是废话一样)。那我们只要了解这个类有哪些函数,函数作用是什么就可以了。贴个简化的源码,我只写了几个常用的函数,主要我觉得把这几个都搞懂了,其他的就也懂了。
class condition_variable { // class for waiting for conditions
public:
using native_handle_type = _Cnd_t;
condition_variable() {}
~condition_variable() noexcept {}
condition_variable(const condition_variable&) = delete;
condition_variable& operator=(const condition_variable&) = delete;
void notify_one() noexcept { // wake up one waiter
}
void notify_all() noexcept { // wake up all waiters
}
void wait(unique_lock<mutex>& _Lck) { // wait for signalnothrow
}
template <class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) { // wait for signal and test predicate
while (!_Pred()) {
wait(_Lck);
}
}
};
还是贴下官方文档的
正常我们学习,写一些玩具代码,只会用到 notify_one、wait这俩函数,真的相信我,不过这俩懂了,条件变量也就懂了。
1.wait函数(理解了这个函数,条件变量就懂了)
void wait(unique_lock<mutex>& _Lck)
template <class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)
作用:阻止当前线程直到条件变量唤醒(官翻)。
当我们调用了这个函数,线程就会阻塞,不往下执行,参数中的Mutex,在调用后,条件变量内部会自动将其释放,解锁。
这个函数wait(unique_lock<mutex>& _Lck),参数是 mutex。前面概念说到条件变量只是用于线程间等待-通知,但不负责保护共享资源,划重点哦。所以我们实际在写代码的时候,会配合上mutex使用。
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)这个版本就跟下面代码中while一样,后续解释。
贴一个消费者线程通用写法的伪代码
1 std::condition_variable cv;
2 std::mutex mx;
3 //int buffer,用于模拟缓冲区数据,实际应用可能是字符数组或者其他类型,但原理相同
4 int buffer = 0;
5 //假装这是在一个线程里面
6 {
7 std::unique_lock<std::mutex> lock(mx);
8 ///两种写法效果相同, 可以看wait的源码,上面的写法,等价于上面这种,只是觉得上面这个写法更适合我们理解
9 #if 1
10 while( buffer == 0 ) {
11 cv.wait(lock);
12 }
13 #else
14 cv.wait(lock, [&](){ return buffer != 0; });
15 #endif
16//可以对buffer,做某种操作了
}
通过代码解释哈,线程同步的本质就是对共享资源的访问。如上代码,我们先用mutex上锁,因为buffer相当于共享资源,下面的操作,肯定是对buffer操作。
当我们执行到while这里的时候,先判断buffer == 0,等于0,就代表没有数据给我们用,那就执行wait函数,我们调用了wait函数,这个时候此线程就会阻塞到这里,不会接着往下执行了,并且mutex,会释放,让其他线程可以拿到锁,然后对buffer进行操作,那其他线程肯定会将buffer赋值嘛,表示有数据了,然后其他线程就会调用 cv.notify_one 让现在这个线程唤醒,就是接着往下执行,那么下一步就走到了 while的判断这里,此时buffer != 0, 就跳出了循环,往下执行,对buffer进行操作。
这里之所以要把mutex作为参数传给wait函数,就是我们调用了wait后,条件变量内部就会把mutex释放了,好让其他线程拿到锁操作buffer。
大概逻辑就是
线程 调用wait----> 阻塞(其他线程修改buffer, 唤醒此线程)-->接着往下执行,跳出循环--->往下执行
上面代码执行逻辑: 10行(buffer为0) -->11行(线程wait) -->阻塞(等待其他线程唤醒)-->10行(buffer不为0) -->16行。
while( buffer != 0 ) {
cv.wait(lock);
}
可能有人对这里有疑惑,为啥要while,只写wait行不行,不也是会阻塞吗。不行!!!
原因:
1. 条件变量可能会被操作系统虚假唤醒,那这时的buffer可能还是0,那不写while,就会访问到错误的buffer。
2.如果这个线程一开始执行,你的buffer就不是0,那我们就不用wait了。
2.notify_one
void notify_one() noexcept { // wake up one waiter
}
作用:唤醒调用了wait的线程,让其继续往下执行。
这个函数使用起来很简单,就是该线程(生产者线程)修改了共享资源,然后调用notify_one唤醒调用了wait的线程(比如上面说的消费者线程)。
贴一个生产者线程通用伪代码
std::condition_variable cv;
std::mutex mx;
//int buffer,用于模拟缓冲区数据,实际应用可能是字符数组或者其他类型,但原理相同
int buffer = 0;
//假装在一个生产者线程
{
std::unique_lock<std::mutex> lock(mx);
buffer = i;
std::cout << "Producer buffer = " << i << "\t";
cv.notify_one();
}
生产者线程比较简单, 就是拿到mutex的锁后,修改共享资源后,调用notify_one()唤醒在等待的线程继续往下执行。
4. 完整使用示例
1. 经典生产者-消费者模型
可能有以下两种情况:
1. 线程1先执行:它拿锁,设置 i = 1,然后 notify_one(),线程2还没到 wait(),这时线程2后续执行时发现 i == 1,while( i != 1) { cv.wait(lock); } 立即通过,不会阻塞。
2. 线程2先执行:它拿锁,调用 wait(),这时会释放锁并阻塞等待;线程1后执行,拿锁,修改 i = 1,调用 notify_one(),唤醒线程2,线程2拿回锁,检查 i == 1 条件成立,继续执行。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mx;
std::condition_variable cv;
int i = 0;
int main() {
// 启动线程1:设置 i=1 并通知
std::thread t1([&](){
std::unique_lock<std::mutex> lock(mx);
i = 1;
cv.notify_one(); // 通知等待线程
});
// 启动线程2:等待 i==1 后打印
std::thread t2([&](){
std::unique_lock<std::mutex> lock(mx);
while( i != 1)
{
cv.wait(lock);
}
std::cout << "Thread ID: " << std::this_thread::get_id() << " i=" << i << std::endl;
});
// 等待线程结束
t1.join();
t2.join();
return 0;
}
2.实战-简化项目代码(网络收数据)
实际项目的情况,比如我们收到网络数据,一个线程收数据,另一个线程解析数据,这也是生产者消费者模型的场景。 收数据的线程,有数据后,写入缓冲区,然后通知解析线程运行。
因为我是垃圾Qt仔,就使用的Qt的网络库写的示例代码。
#include <QApplication>
#include <QThreadPool>
#include <condition_variable>
#include <mutex>
#include <QTcpServer>
#include <QTcpSocket>
//模拟实际解析数据帧
int FrameLength = 0;
void process(QByteArray *data)
{
while (true) {
if (data->size() < FrameLength)
break;
int headerIndex = data->indexOf(char(0xF5));
if (headerIndex == -1)
break;
// 数据不足以构成一帧
if (data->size() - headerIndex < FrameLength)
break;
QByteArray frame = data->mid(headerIndex, FrameLength);
if (frame.endsWith(char(0x5F))) {
data->remove(headerIndex, FrameLength);
qDebug() << "解析到帧:" << frame.toHex(' ').toUpper();
} else {
// 避免跳过合法帧头
data->remove(headerIndex, 1);
}
}
}
int main(int argc, char *argv[])
{
//模拟线程什么时候该退出,比如实际应用时,软件退出
std::atomic<bool> bExit = false;
std::atomic<int> sendDataSize = 0, recvDataSize = 0;
//模拟发数据的一端
QThreadPool::globalInstance()->start([&](){
QTcpServer tcpServer;
qDebug()<< "---------->TcpServer.listen:" << tcpServer.listen(QHostAddress("127.0.0.1"), 8001);
if (tcpServer.waitForNewConnection(3000))
{
QTcpSocket *socket = tcpServer.nextPendingConnection();
QByteArray data = QByteArray::fromHex("F5123456785F");
FrameLength = data.size();
sendDataSize = data.size() * 50;
for(int i = 0; i < 50; ++i)
{
socket->write(data);
socket->waitForBytesWritten();
// QThread::msleep(500);
}
QByteArray data2 = QByteArray::fromHex("F5785634125F");
sendDataSize += data2.size();
socket->write(data2);
socket->waitForBytesWritten();
}
qDebug()<< "------------->Send data is end.";
});
QByteArray m_buffer, m_bufferBak;
std::condition_variable cv;
std::mutex mx;
//解析线程
QThreadPool::globalInstance()->start([&](){
while(true) {
std::unique_lock<std::mutex> lock(mx);
//判断此时是否有数据来,没有的话,就等
cv.wait(lock, [&](){
return !m_buffer.isEmpty();
});
if (!m_buffer.isEmpty()) {
//迅速把缓冲区的数据取出来,然后解锁,让收数据的线程继续工作
//不然因为解析的时间过长,影响收数据线程工作。
QByteArray recvData = m_buffer;
m_buffer.clear();
lock.unlock();
recvDataSize += recvData.size();
//解析数据逻辑
m_bufferBak.append(recvData);
process(&m_bufferBak);
}
if (recvDataSize == sendDataSize) {
bExit = true;
break;
}
}
});
QTcpSocket client;
client.connectToHost(QHostAddress("127.0.0.1"), 8001);
qDebug() << "client.waitForConnected()=" << client.waitForConnected();
//模拟收数据线程,为了方便就在主线程写。
while (true){
if (bExit)
break;
if (client.waitForReadyRead()) {
QByteArray data = client.readAll();
//有数据,写到缓冲区后,马上通知解析线程处理
std::unique_lock<std::mutex> lock(mx);
m_buffer.append(data);
cv.notify_one();
}
}
QThreadPool::globalInstance()->waitForDone();
return 0;
}
最后
初次发稿,写得不好的地方,请指正,或有不明白的地方,在评论区指出后,在修改内容。