线程和并行编程之线程和QObjects(六)

QThread继承于QObject,它会发送信号标识线程的开始与结束。更有趣的是QObjects会用于多线程中,发送的信号会调用另外线程中的槽函数、发送事件给另外线程的对象。实现这种机制的基础:每个线程可以拥有独自的事件循环。

对象QObject可重入

QObject是可重入的。大部分非GUI类的子类,如QTimer、QTcpSocket、QUdpSocket、QProcess等也是可重入的,多线程可同时使用这些类。记住一点:这些类被设计成在一个线程中创建和使用的。在一个线程中创建对象,但是调用另外一个线程中的方法,行为不保证一定有效!由三点限制需要注意:

  1. QObject的子类必须在创建父对象的线程中被创建。也就是一个对象QObjectA是在QThread中被创建,那么就不能将QThread(this)当成父传递给QObjectA(因为QThread本身是在另外一个线程中创建的)。
  2. 事件驱动的对象只能在单个线程中使用。主要对于QTimer机制、网络模块,如何在子线程中定时的操作?!
  3. 确保QThread被释放前,所有在QThread内部创建的对象都已经被释放。一种简单方式:QThread::run的栈空间分配内存。

尽管QObject是可重入的,但是GUI类-QWidget及其子类是不可重入的。他们只能用于主线程中,并且在主线程启动前必须调用QCoreApplication::exec()。

线程事件循环

一个线程中的事件循环,可以使该线程使用非GUI的类(需要在事件循环中运行,如QTimer、QTcpSocket、QUdpSocket等),并且可以将信号连接到其他线程的槽函数中。

线程和并行编程之线程和QObjects(六)_第1张图片

  • QObject::thread: QObject存活于创建它的线程,事件传递到Object是由线程的事件循环分发的。可通过QObject::thread()查看QObject在那个线程。
  • QObject::moveToThread:更改一个线程及其子对象的线程归属。(如果对象设置父了,就不能转移线程了)

如果事件循环未运行,事件不会分发到Object中。例如,在线程中创建QTimer对象,但是没有调用exec(),该Timer对象不会发送信号timeOut()。同样的,调用deleteLater也不会生效(这些限制也在主线程中同样适用)。

可以在任意时刻任意线程中,使用线程安全的方法QCoreApplication::postEvent()向对象发送事件。该事件会被自动的分发到创建该对象所在线程的事件循环中。所有线程都支持事件过滤器,限制是:监控对象和被监控对象必须在同一个线程中。QCoreApplication::sendEvent() 发送的事件要求发送方和接收方在同一个线程中。

从其他线程访问QObject的子类

QObject及其子类都不是线程安全的。这包括整个事件传递系统。记住一点:当你从其他线程访问对象时,事件循环会传递事件到你的QObject子类。如果在不是当前线程的类上调用一个方法,该对象可能接收事件,因此必须要对内部共享变量加锁,否则会发生非预期行为。

QThead对象存活于该对象创建所在的线程,因此线程子类中的槽函数要对共享变量加锁。另一方面,可以在QThread::run实现中发送信号,因为发送信号时线程安全的。

有如下场景:需要在子线程中定时的执行某些操作,操作的结果更新到主线程中。如何实现呢?以下是详细的代码实现:

以下是头文件:

#pragma once
//
//在子线程中执行定时的操作

#include 
#include 
#include 
#include 

//子线程工作单元
class TimerWorkerItem : public QObject
{
	Q_OBJECT
public:
	TimerWorkerItem();

public slots:
	void startTimer();
	void endTimer();

private slots:
	void doSomething();

private:
	QTimer* m_pTimer;
};

//主线程控制类
class Manager : public QObject
{
	Q_OBJECT

public:
	Manager();

signals:
	void startTheadTimer();
	void endTheadTimer();

private:
	TimerWorkerItem* m_pWorkerItem;
	QThread m_WorkderThread;
};

void testTimerThreadDemo();

以下是源文件:

#include "TimerThreadDemo.h"

//
//子线程工作单元
TimerWorkerItem::TimerWorkerItem()
:m_pTimer(nullptr)
{
	//该对象在主线程中构造
	qDebug() << "TimerWorkerItem::TimerWorkerItem   MainThread:" << QThread::currentThread();
}

void TimerWorkerItem::startTimer()
{
	if (!m_pTimer)
	{
		//在子线程中实例化定时器
		m_pTimer = new QTimer;
		m_pTimer->setInterval(2000);
		connect(m_pTimer, SIGNAL(timeout()), this, SLOT(doSomething()));
		qDebug() << "TimerWorkerItem::startTimer   ChildThread:" << QThread::currentThread();
	}

	if (!m_pTimer->isActive())
		m_pTimer->start();
}

void TimerWorkerItem::endTimer()
{
	if (m_pTimer->isActive())
	{
		//在子线程中释放定时器
		m_pTimer->stop();
		delete m_pTimer;
		qDebug() << "TimerWorkerItem::endTimer   ChildThread:" << QThread::currentThread();
	}
}

void TimerWorkerItem::doSomething()
{
	//在子线程中执行槽函数
	qDebug() << "to do something...";
	qDebug() << "TimerWorkerItem::doSomething   ChildThread:" << QThread::currentThread();
}

//
//主线程控制类
Manager::Manager()
{
	//不能设置父对象!!!否则不能转移对象的线程归属!
	m_pWorkerItem = new TimerWorkerItem;
	//移动对象所在线程
	m_pWorkerItem->moveToThread(&m_WorkderThread);

	//绑定主子线程的信号槽
	connect(&m_WorkderThread, SIGNAL(finished()), m_pWorkerItem, SLOT(deleteLater()));
	connect(this, SIGNAL(startTheadTimer()), m_pWorkerItem, SLOT(startTimer()));
	connect(this, SIGNAL(endTheadTimer()), m_pWorkerItem, SLOT(endTimer()));

	//启动线程
	m_WorkderThread.start();

	qDebug() << "Manager::Manager   MainThread:" << QThread::currentThread();

	//方式1. 调用子线程中函数,直接调用另外线程中的函数A,函数A会在主线程中执行,这样是不对的。
	//m_pWorkerItem->startTimer();

	//方式2. 调用子线程中函数,通过信号槽的方式调用另外线程中的函数A,这样才是对的。
	emit startTheadTimer();
}

//o
void testTimerThreadDemo()
{
	Manager* pManager = new Manager;
}

请注意上述调用子线程中的函数不同方式。

  1. 方式一执行结果是错误的,请看输出信息:
TimerWorkerItem::TimerWorkerItem   MainThread: QThread(0x11ac1f8) //对象所在线程默认是:创建该对象时的所在线程
Manager::Manager   MainThread: QThread(0x11ac1f8)
TimerWorkerItem::startTimer   ChildThread: QThread(0x11ac1f8) //这个是在主线程中执行的,不对。
to do something... 
TimerWorkerItem::doSomething   ChildThread: QThread(0x11d1014)
  1. 方式二执行结果是错误的,请看输出信息:
TimerWorkerItem::TimerWorkerItem   MainThread: QThread(0xffa6f8) //对象所在线程默认是:创建该对象时的所在线程
Manager::Manager   MainThread: QThread(0xffa6f8)
TimerWorkerItem::startTimer   ChildThread: QThread(0x1020dd4)  //这个是在子线程中执行的,正确。
to do something...
TimerWorkerItem::doSomething   ChildThread: QThread(0x1020dd4)

通过上述的案例,使用了QThread、moveToThread、工作单元、定时器的协同处理。

跨线程的信号槽连接

Qt支持如下几种信号槽连接类型:

  1. Auto Connection:默认连接方式,若发送者和接收者在同一个线程,那么连接方式同Direct Connection。否则同Queued Connection。
  2. Direct Connection:若发送者和接收者在同一个线程。信号发送后,槽函数立即被调用,且在发送者所在的线程中执行。
  3. Queued Connection:若发送者和接收者不在同一个线程。信号发送后,槽函数会在控制权返回到接收者的事件循环中,在该线程中执行。
  4. Blocking Queued Connection:与Queued Connection类似,区别是该线程会阻塞直到槽函数执行完。发送者和接收者在同一个线程中会引发死锁。
  5. Unique Connection:与Direct Connection类似,区别是只有不重复的连接时才有效。

通过Connect()函数能够指定信号槽的连接形式。如果发送者和接收者不在同一个线程,连接类型用直接连接,这样就是不安全的。同理,直接调用其他线程中的对象的任何函数都是不安全的。QObject::connect本身是线程安全的。

你可能感兴趣的:(Qt剖析,Qt,可重入,事件循环,信号槽)