C++/Qt 多线程编程分析

本文围绕Qt框架下的多线程编程展开深入讨论,解析多线程的意义与应用场景,探讨多线程同步机制,分析子线程挂起、阻塞的影响,并通过实例演示多线程与消息队列的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

几个疑问

  • 单核/多核系统下,多线程访问冲突问题?
  • 多线程用了好几年,问你其意义何在,你却欲言又止?
  • 子线程挂起和短暂睡眠,对应用程序的运行效率有什么影响?
  • 在单核系统中跑多线程应用,抛却效率不谈,会有什么好处吗?
  • 嵌入式系统(DSP/ARM etc.),中断/任务调度、CPU时间片的概念清楚吗?
  • 多线程执行流程同步与共享数据加锁(semaphore&mutex)的真正区别?

用上面这几个问题,作为引子,在该文章中展开基于Qt的多线程编程的讨论与分析!

为啥非要用多线程?

引发这个思考的原因是,有个领导不喜欢多线程,其认为在嵌入式单核Arm上影响运行效率。自己也知道,使用多线程时,常常,内存冲突、难运行调试、线程同步易出错等问题如影随之,那为什么还要用呢?在查阅资料前我有这么几点自己的总结:

  1. 在多核心CPU下的,通过CPU调度实现线程并行执行
  2. 使用多线程有时候会让软件层次结构更加清晰。
  3. 耗时操作(如读写设备)必须要在子线程中执行,这应算是并发执行的一种体现。
  4. 为了提升响应时机,而不是关注整体运行的时间和。<后记>其实这有点类似是并发执行的概念,即感觉同时发生。
    4.为了实现某种循环过程,通常伴随挂起、阻塞、同步等情景。

复习几个概念

单核心CPU、多核心CPU、进程调度、线程调度 ,线程是CPU调度和分配的基本单位,进程是操作系统进行资源分配(包括cpu、内存、磁盘IO等)的最小单位,一定要区别清楚。操作系统的调度?CPU调度?进程调度?线程调度?

子线程挂起

多线程的代价及上下文切换
线程的睡眠/挂起/阻塞

多线程安全

同步与共享数据锁
在使用多线程的过程中,也用信号量也用锁,但其实一直搞不明白,自己到底是在做同步还是做保护,因为潜意识里,同步了就不用锁了,那么锁mutex的实现意义究竟在哪里?

曾经产生了这么一个想法,同步与锁,一个是等一个是抢。一个是在制定规则,保证顺序性使得资源有序访问,锁的本质是打架,没有具体的顺序可言?如,加锁的共享数据,可能写N次才有机会读一次。后来否定了这个想法!可是忘记什么理由了,留坑继续想吧以后!

多线程消息队列

利用多线程可以通过并行执行任务来提升效率,但是很多场景下,不是所有的任务都是可以一起执行的,现实情况是有的任务必须要等到之前那些可并行的任务都执行后才可以继续执行的。考虑如下任务场景,从任务链出发,分辨出其中的可并行及需要串行的任务子链。

多线程与消息队列似乎通常是形影不离的,队列是消息机制的一种具体形式吗?要不要搜索下,多线程与消息机制?

是否跨线程传递数据

跨线程但是不传递数据
可能是这样的一个场景,纯粹的是为了同步,一个过程结束后再进行另一个数据,可能此时QueueConnection链接的信号槽没有任何的参数,这时候,跨线程转接不必担心数据安全。

跨线程Queue转接传递数据(传递指针)
这种情况即使加锁也不能保证数据的安全性,也就是说使用Queueconnection链接带指针参数的信号槽,无法保证数据安全。因为:该情况下(指针的的写操作-线程1)与(指针的读操作-线程2),无法保证其一对一过程,控制不了线程调度情况。详细点,我们可以假设线程2有耗时操作、或者系统过阵子才能调度到2线程,则会出现:线程1执行两次写过程,才执行一次线程2读过程。至于,线程1第二次写的时候碰上线程2正在读,不会生,因为单次的读写过程是有锁的–

跨线程Queue转接传递数据(传值)
传值就会发生参数拷贝过程,不知道QueueConnection模式有没有保证这个内部过程的安全性?会不会这里的值拷贝被放到一个对列缓冲区中,不过可以肯定的是,这比传指针安全的多!

跨线程的共享数据安全
当使用直接连接进行信号槽连接时,其中涉及到的数据安全问题,与直接上锁硬操作没有区别。回头一想,在传递指针时提到的问题,这里同样会有,“无法保证一对一过程”,不应该归到共享数据安全问题中,它应该更接近于“生产者与消费者不平衡”,如果有不平衡可能,应该去加缓冲,如果无关紧要,则认为能保证读写过程平衡。

HMI假死

该现象原本与多线程关系不大,因为只要在主线程中干耗时工作(如读写文件操作),就会卡。无奈,有时候在所难免的需要在主线程与子线程之间共享数据,这时通常有锁伴随,如果子线程长时间持有锁不释放,那么就有了“子线程导致HMI卡顿”,其实主线程不怕上锁,它怕持续的得不到锁。因此跨主线程的锁,不能在他处逗留太久(如每次不能超过45ms),但是允许频繁串门子!
如果在主线程中进行套接字接收read操纵,交互非常快,HMI会假死吗?这里如果使用QIODevice::redyRead信号机制,read讲成为异步操作,耗时很少(这是根据串口类QextSerialPort官帮总结的,不确定完全对!)- 即HMI线程,真的不怕高频吗?

另外的,在Qt中,若使用队列链接的信号槽,持续的向主线程发送信号,可能会使得主线程进入忙碌状态,无暇相应HMI操作,从而假死!这里更趋向于,认为这种卡顿与读文件耗时导致卡顿相似(主线程在忙着处理队列中的排队消息,要过很久才能轮到人机操作相应?)至于深层次根本原因,容后再议!这里我们再详细下这个测试过程:分别睡眠0us,1us,10us、1ms,是不是1u的睡眠就会让HMI脱离假死?还是说这个睡眠间隔必须的要大于其所触发的主线程任务的处理时间?具体测试代码与结论:

到这,对原先一个脑壳里的问题,又清新了些:原先担心Qt的signals队列溢出,会导致hmi假死,那现在我可以问:signals队列此时堆积率80%,应用还能撑住没死,这时候HMI卡不卡?当然卡啊,这是顺序队列吧,先来后到的处理啊!

Qt子线程实现方式再分析

2017年初,曾经有学习过此部分,现在重新思考。

使用Work方式,淡化了子线程入口函数的概念,线程的入户口函数是隐藏的,我们可以将任意的work槽函数作为一个虚拟的 入口。那么原理呢?也就是movetothread的这一功能的实现原理?

pWork->moveToThread(&aThread);
aThread.start();    //线程启动
/*! @brief
*/

跨线程的信号槽链接

QObject官帮,Note: All functions in this class are reentrant, but connect(), connect(), disconnect(), and disconnect() are also thread-safe.也一直在用Qt::QueuedConnection参数,但是却一直不知道这是什么原理?它是信号量或者是锁实现的吗?<后记1>这里更应该是CPU线程调度原理的解释范畴?

定时器碰撞子线程阻塞

//bluework.h
class CBlueWork : public QObject
{
	...
public slots:
	void DoLoopWork();
	void QueryTimeroutEmit();
	void QueryTimeroutSlot();
private:
	QTimer *m_pTimer;
	bool    m_bStopLoop; //stop die loop
}

//bluework.cpp
CBlueWork::CBlueWork(QObject *parent) : QObject(parent)
{
	m_bStopLoop = false;
	m_pTimer = new QTimer();
	m_pTimer->setInterval(2000);
	//查询定时器触发信号的发送者线程
	connect(m_pTimer, SIGNAL(timeout()), this, SLOT(QueryTimeroutEmit()), Qt::DirectConnection);
	//查询定时器触发信号的相应者者线程
	connect(m_pTimer, SIGNAL(timeout()), this, SLOT(QueryTimeroutSlot())); //Qt::QueuedConnection
	m_pTimer->start();   //start timer 
}
void CBlueWork::QueryTimeroutEmit()
{
	qDebug() << "Work Timer out Emit From " << QThread::currentThreadID();
}
void CBlueWork::QueryTimeroutSlot()
{
	qDebug() << "Work Timer out Slot run in  " << QThread::currentThreadID();
}
void CBlueWork::DoLoopWork()
{
	static int i = 0;
	while (!m_bStopLoop)
	{
		i++;
		if (0 == i % 1000)
		{ qDebug() << "--Do Loop Work---  "  << i << QThread::currentThreadID(); }
		else
		{ usleep(5000);   //include <unistd.h> }
	}
}
//运行管理 widget.cpp
void Widget::on_pushButtonStartThreadnWorkLoop()
{
	m_pWork = new CBlueWork();
	m_pThread = new QThread();
	m_pWork  = m_pThread->moveToThread(m_pThread );
	
	//附加//进一步的测试 //非规范代码 //标记为 </color 测试2/>
	//m_pWork ->m_pTimer->moveToThread(m_pThread ); 
	
	QObject::connect(m_pThread , SIGNAL(started()), m_pWork  , SLOT(DoLoopWork())); 
	m_pThread->start();
}
void Widget::on_pushButtonStartThreadnWorkLoop()
{
	m_pWork ->StopLoop();  //m_bStopLoop=true;
}

调试信息:

测试1测试2
Main Thread 0x25d4
Work Timer out Emit From 0x25d4
Work Timer out Emit From 0x25d4
–Do Loop Work— 1000 0x276c
Work Timer out Emit From 0x25d4
Work Timer out Emit From 0x25d4
–Do Loop Work— 2000 0x276c
Work Timer out Emit From 0x25d4
Work Timer out Emit From 0x25d4
/----------Execute Loop work--------/
Work Timer out Slot run in 0x276c //1
Work Timer out Slot run in 0x276c //2
Work Timer out Slot run in 0x276c
Work Timer out Slot run in 0x276c
Work Timer out Slot run in 0x276c
Work Timer out Slot run in 0x276c //6
Work Timer out Emit From 0x25d4
Work Timer out Slot run in 0x276c
Work Timer out Emit From 0x25d4
Work Timer out Slot run in 0x276c…
Main Thread 0x2444
–Do Loop Work— 1000 0x23e4
–Do Loop Work— 2000 0x23e4
–Do Loop Work— 3000 0x23e4
–Do Loop Work— 4000 0x23e4
–Do Loop Work— 5000 0x23e4
–Do Loop Work— 6000 0x23e4
/----------Execute Loop work--------/
Work Timer out Emit From 0x23e4
Work Timer out Slot run in 0x23e4
Work Timer out Emit From 0x23e4
Work Timer out Slot run in 0x23e4

几点总结

  1. 定时器运行在其创建线程(mainthread)-(因没有m_pTimer->moveToThread执行,其形式上的所属对象move并木有影响到timer自身线程)
  2. m_pThread死循环一旦停止,原先积攒的timerout信号被一股脑执行(没丢)
  3. 一个可能一直理解错误的问题:U无法响应向死循环子线程发送信号的过程! 难 道 不 是 没 发 进 去 ( 子 线 程 事 件 循 环 队 列 ) ? \color{red} 难道不是没发进去(子线程事件循环队列)? 线只是没有排上响应时间?堆积到多少开始丢失?堆积信号的持续处理等同于some type endless loop?
  4. 测 试 2 \color{#4285f4}{测试2} 2中,定时器的Emit也会被打住,一直到死循环结束,Emit与Slot才开始同步执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值