C++新特性35_条件变量的引入(实现线程间数据传递、线程执行顺序;传统采用while(1)轮询方式解决线程按顺序执行和共享变量问题低效,用条件变量可以实现高效的事件模型(类似Qt信号槽和事件机制))

本文介绍了C++中线程同步的问题,从使用mutex解决共享变量访问的安全性,到通过条件变量实现线程间的通信和顺序执行。通过示例展示了在没有条件变量时,线程可能会遇到的执行顺序问题和效率低下的轮询解决方案,并引出了条件变量作为更高效的通知机制。条件变量允许主线程在满足特定条件时通知子线程,提高了多线程通信的效率。

前几篇介绍了使用mutex互斥体实现线程同步,操作系统中另外一个重要的事情就是线程之间实现通信。线程之间通信包含以下两种情形:

  • 线程之间可以通信 即线程A B间的数据可以相互传递
  • A 与B线程之间是有先后的执行顺序的,一种情况是A线程在完成某个操作之后再运行B线程

上述两种情况,使用互斥体mutex就可能显得不是很合适了,因为mutex的目的是为了以一种更加安全的方式去访问共享的变量,但无法做到线程间的数据通信或者实现线程之间逻辑的关系。因此引入新的控制同步的条件变量

C++11中是直接将Linux的机制封装,此处先讲在C语言中是如何实现的,再对C++中如何封装进行讲述,最后讲条件变量的使用场合。

1. 举例1:线程间存在先后的执行顺序和共享变量存在的问题

下例为对一个公共的string变量在线程A和B中进行传递,预期逻辑 A先传递信息给字符串,字符串信息后传递到B

//#include "stdafx.h"
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <future>

using namespace std;

//全局互斥体,用于同步
std::mutex g_mtx;

//用于发送消息
std::string str;

//预期逻辑:A传递信息给字符串,字符串信息传递到B

//B 子线程回调函数
void worker_thread()
{
	printf("%s",str.c_str());
}


//A 主线程
int main()
{
	//创建B线程,随时可能执行,
	//很可能A还没有赋值B已经跑起来了
	std::thread thd(worker_thread);
	//使当前主线程睡眠1秒钟,使得CPU有机会切换执行子线程
	std::this_thread::sleep_for(std::chrono::seconds(1));
	str = "hello world";
	thd.join();
}

运行结果:主线程中回调用子线程的回调函数,在创建了B线程之后,随时可能切换到B线程中,导致还来不及赋值就跳转到B线程,导致打印出现问题(什么都没输出)。
在这里插入图片描述

2. 举例2:使用锁以轮询方式解决线程间存在先后的执行顺序和共享变量的问题

为了解决上面的问题:使用锁保证共享变量的同步操作,利用标志位g_isSet和while(1)进行线程A和B是否完成赋值的检查,具体代码如下:

//#include "stdafx.h"
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <future>

using namespace std;

//全局互斥体,用于同步
std::mutex g_mtx;

//用于发送消息
std::string str;

//增加标志位
bool g_isSet = false;

//预期逻辑 A先传递信息给字符串,字符串信息再传递到B

//问题:
//1.反应迟钝,等待时间长
//2.B线程不断循环(轮询),条件不成功,则休眠等待

//B 子线程回调函数
void worker_thread()
{
	//利用while(true)死循环不断查看标志位状态
	while (true)
	{
		std::lock_guard<std::mutex> lk(g_mtx);
		//判断标志位
		if (g_isSet)
		{
			printf("%s", str.c_str());
			break;
		}
		else {
			//条件未满足将子线程休息一会再循环
			std::this_thread::sleep_for(std::chrono::seconds(1));
		
		}
	}
}


//A 主线程
int main()
{
	//创建B线程,随时可能执行,
	//很可能A还没有赋值B已经跑起来了
	std::thread thd(worker_thread);
	{
	//由于使用了共享变量,因此需要进行同步设置
		std::lock_guard<std::mutex> lk(g_mtx);

		//使当前主线程睡眠1秒钟,使得CPU可切换执行子线程
		std::this_thread::sleep_for(std::chrono::seconds(1));

		str = "hello world";
		//设置完成后标志位为true
		g_isSet = true;
	}
	
	thd.join();
}

运行结果如下:
在这里插入图片描述
虽然上面的代码解决了对公有变量操作问题,但是等待时间过长的问题,这是由于B线程不断循环,条件不成功,则休眠等待

整个过程简单描述: 主线程A比作快递员送快递,子线程B比作我们本身取快递,比较快捷的方式就是快递到了之后,快递员告知我们去取,上面的代码就变成了我们不知道快递什么时候到,我们就不断地去查看快递是否到货,这种方式就是轮询,如果没到我们休息一会再去查看。

3. 条件变量的引入

轮询的方式十分低效,实际采用快递到货后通知是最高效的,这种在计算机中称为事件模型,通知方式,对于上例中需要达到的效果:主线程A在完成字符串信息传递之后,发送消息给线程B,此时再执行线程B中的操作

这种经典方式在操作系统(R3&R0)内部有大量使用,Windows内部有大量的event事件(Qt中的event应该也是采用此种方式),网络中iocp epoll kevent都可以使用事件的模式操作

这种思想可以使用条件变量来完成当前多线程通信或者说线程先后顺序执行,下篇将会讲条件变量是如何完成事件模型的

4. 学习视频地址:条件变量的引入

<think>我们讨论的是自定义继承QThread的线程类,在run函数中有一个while循环轮询设备。我们通过一个成员变量m_running(通常用bool类型)来控制循环是否继续。同时,我们提供了一个stopPolling()函数,用于将m_running设置为false,以请求线程停止。 关键问题:在while循环运行期间,当我们在主线程(或其他线程)调用stopPolling()时,m_running的修改能否被轮询线程及时感知? 答案:可以,但需要注意以下几点: 1. **可见性问题**:由于线程可能运行在不同的CPU核心上,每个核心有自己的缓存,因此一个线程变量的修改可能不会立即被另一个线程看到。在C++中,对bool类型的读写通常被认为是原子的(因为bool通常是一个字节,而单字节的读写在大多数体系结构上是原子的),但这并不能保证可见性。因此,我们需要使用同步机制来确保修改后的值对其他线程可见。 2. **使用互斥锁(QMutex)**:在stopPolling()函数中,我们使用互斥锁来保护对m_running的修改,同样在run()函数中读取m_running时也要用同一个互斥锁保护。这样做的目的是: - 互斥:确保同一时间只有一个线程访问m_running。 - 内存屏障:确保在锁释放时,当前线程变量的修改会写回主内存,并且在获取锁时,从主内存重新加载变量,从而保证可见性。 3. **无锁方式**:也可以使用原子类型(如`std::atomic<bool>`或Qt的`QAtomicInt`),但这里我们主要讨论使用QMutex的情况。 根据引用[2]引用[5],我们需要注意: - 在stopPolling()中,我们通过互斥锁修改m_running,然后通知线程退出。 - 在run()的循环中,我们每次循环都会检查m_running,并且检查时也要加锁(或使用原子操作)。 因此,在while循环中,我们通过加锁来读取m_running,这样就能保证我们能够及时看到其他线程对m_running的修改。 但是,需要注意的是,如果我们的循环体执行时间很长(比如一次轮询需要几秒钟),那么即使我们调用了stopPolling(),也要等待当前这一次轮询完成后,下一次循环条件检查时才能看到m_running的变化。所以,如果希望线程能够更及时地响应停止请求,可以将循环条件检查放在循环体内多次进行,或者将长时间的操作拆分成多个步骤,在步骤之间检查停止标志。 在我们的实现中,轮询循环如下: ```cpp void DevicePollingThread::run() { { QMutexLocker locker(&m_mutex); m_running = true; } while (true) { // 检查停止标志 { QMutexLocker locker(&m_mutex); if (!m_running) break; } // 读取设备参数并发送信号 QVariantMap params = readDeviceParameters(); emit parametersRead(params); // 精确间隔控制 QThread::msleep(m_interval); } } ``` 在每次循环开始时,我们都会检查m_running。因此,当我们在主线程调用stopPolling()(该函数内部会加锁并将m_running置为false)后,在轮询线程的下一次循环条件检查时,就会看到m_running为false,从而退出循环。 所以,答案是肯定的:在while循环轮询执行时,能够收到stop信号并将m_running置为false,并且会在下一次循环条件检查时退出循环。 **注意**:这里有一个细节,我们在循环体内使用了`QThread::msleep`,这个睡眠期间,线程是阻塞的。但是,在睡眠期间,如果调用了stopPolling(),那么当前循环条件检查已经完成,正在睡眠,所以必须等待睡眠结束,进入下一次循环条件检查才能看到变化。如果希望线程在睡眠期间也能及时响应停止请求,可以使用更短的睡眠间隔,或者使用条件变量(QWaitCondition)来替代简单的睡眠。 总结: - 使用互斥锁保护m_running,确保修改的可见性。 - 在循环体内定期检查停止标志。 - 对于长时间的操作,考虑更频繁地检查停止标志或使用条件变量。 §§相关问题§§ 1. 如何确保多线程环境下共享变量的可见性? 2. 在Qt中,除了使用互斥锁,还有哪些方法可以实现线程安全? 3. 如何实现线程睡眠期间也能响应停止请求? 4. 使用原子类型(如std::atomic)与互斥锁相比有什么优缺点? </think> ### 自定义QThread线程轮询中响应停止信号的机制分析 是的,自定义继承`QThread`的线程在`while`循环轮询执行时,**能够收到stop信号并将`m_running`置为`false`**,但需要满足以下关键条件: #### ✅ 正确实现机制 1. **互斥锁保护共享变量** 必须使用`QMutex`保护`m_running`标志位,确保线程安全: ```cpp // 停止函数实现 void DevicePollingThread::stopPolling() { QMutexLocker locker(&m_mutex); // 加锁 m_running = false; // 安全修改标志位 } ``` 2. **循环内定期检查标志位** 在`run()`的循环中必须**频繁检查**标志位状态: ```cpp void DevicePollingThread::run() { while (true) { // 每次循环都检查停止标志 { QMutexLocker locker(&m_mutex); if (!m_running) break; // 检测到停止信号立即退出 } // ...执行设备轮询操作... } } ``` 3. **信号触发时序保障** - 当主线程调用`stopPolling()`时,通过互斥锁确保`m_running`修改**立即写入主内存** - 子线程在下次循环检查时(通常在毫秒级延迟内)能感知到变化[^1][^2] #### ⚠ 注意事项 1. **响应延迟问题** 如果单次轮询操作耗时过长(如超过500ms),停止响应会有延迟。优化方案: ```cpp while (m_running) { QElapsedTimer timer; timer.start(); // 执行分段操作 doPartialWork(); // 拆分为小任务 // 分段检查标志位 if (!checkRunningFlag()) break; // 精确控制间隔 qint64 elapsed = timer.elapsed(); if (elapsed < m_interval) { QThread::msleep(m_interval - elapsed); } } ``` 2. **原子操作替代方案** 使用`std::atomic<bool>`可获得更高效的无锁实现: ```cpp // 头文件声明 #include <atomic> std::atomic<bool> m_running{true}; // 停止函数无需锁 void stopPolling() { m_running = false; } // 循环中直接检查 while (m_running.load()) { ... } ``` #### ❌ 错误实现示例 以下实现**无法**保证及时响应停止信号: ```cpp // 错误1:未加锁的共享变量 void run() { while (m_running) { // 可能读取缓存旧值 // 长时间操作(>1秒) readDeviceData(); } } // 错误2:循环内未检查标志位 void run() { while (true) { // 执行完整个操作才检查 fullPollingProcess(); // 耗时操作 QMutexLocker locker(&m_mutex); if (!m_running) break; // 响应严重延迟 } } ``` #### 💡 最佳实践建议 1. **循环检查频率**:至少每100ms检查一次标志位 2. **锁范围优化**:用`{}`限定锁的作用域,减少锁持有时间 3. **退出清理**:重写`QThread::terminate()`处理紧急终止 4. **信号通知**:线程退出时发送`finished()`信号[^3] > 通过合理使用互斥锁标志位检查,自定义线程完全能在轮询过程中及时响应停止请求,典型响应延迟在1-50ms之间(取决于循环执行频率)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十月旧城

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值