目录
前言
因为在图像处理的过程中,通过信号与槽避过了需要创建线程的麻烦,但是之后程序的运行出现卡顿的现象。所以想对信号与槽的机制进行了解,但感觉好像和Qt的事件循环有关系,Qt也只是入门而已!对线程的使用和对事件循环的理解感觉是一个很重要的话题!我们到底应该怎样使用信号与槽?怎样使用线程?
学习!分享!感谢!
介绍
Qt中创建、运行线程的“易用”性、缺乏相关编程尤其是异步网络编程知识或是养成的使用其它工具集的习惯、这些因素和Qt的信号槽架构混合在一起,便经常使得人们自己把自己射倒在了脚下。
- 可重入
一个类被称为可重入的:只要在同一时刻至多只有一个线程访问同一个实例,那么我们说多个线程可以安全地使用各自线程内自己的实例。一个函数被称为是可重入的:如果每一次函数的调用只访问其独有的数据,那么我们说多个线程可以安全的调用这个函数。也就是说,类和函数的使用者必须通过一些外部的加锁机制来实现访问对象实例或共享数据的序列化。 - 线程安全
如果多个线程可以同时使用一个类的对象,那么这个类被称为是线程安全的;如果多个线程可以同时使用一个函数体里的共享数据,那么这个函数被称为线程安全的。
对于 类,如果它的 成员函数都可以被不同的线程同时调用而不相互影响,即使这些调用是针对同一个类对象,那么该类被定义为线程安全。对于类,如果其不同实例可以在不同线程中被同时使用而不相互影响,那么该类定义为可重入。
事件和事件循环
一个Qt事件代表了某件令人感兴趣并已经发生的事件,比如鼠标事件; 事件与信号的主要区别在于,**事件**是针对于我们应用中一个**具体目标对象**(而这个对象决定了我们如何处理这个事件),比如鼠标就是一个具体的目标对象,对于这个对象,有按下、弹起、移动等事件,这样我们针对这个事件提供对应的事件处理方法。而信号发射时“漫无目的”的。从代码的角度来说,所有的事件实例是`QEvent`的子类,并且所有的`QObject`的派生类可以重载虚函数`QObject::event()`,从而实现对目标对象实例事件的处理。 事件可以产生于应用程序的内部,也可以来源于外部,比如:- QKeyEvent和QMouseEvent对象代表了与键盘、鼠标相关的交互事件,它们来自于视窗管理程序。
- 当计时器开始计时,QTimerEvent 对象被发送到QObject对象中,它们往往来自于操作系统。
- 当一个子类对象被添加或删除时,QChildEvent对象会被发送到一个QObject对象中,而它们来自于你的应用程序内部。
对于事件来讲,一个重要的事情在于它们并没有在事件产生时被立即派发,而是列入到一个事件队列中,等待以后的某个时刻发送。分配器(dispatcher)会遍历事件队列,并且将入栈的事件发送到目标对象中,因此它们被称为事件循环。从概念上讲,下段代码描述了一个事件循环的轮廓:
while (is_active)
{
while (!event_queue_is_empty)
dispatch_next_event(); // dispatch 调度
wait_for_more_events();
}
我们是通过运行QCoreApplication::exec()
来进入Qt
的主体事件循环的;这会引发阻塞,直至QCoreApplication::exit()
或者 QCoreApplication::quit()
被调用,进而结束循环。
这个wait_for_more_events()
函数产生阻塞,直至某个事件的产生。 如果我们仔细想想,会发现所有在那个时间点产生事件的实体必定是来自于外部的资源(因为当前所有内部事件派发已经结束,事件队列里也没有悬而未决的事件等待处理),因此事件循环被这样唤醒(也就是某个时刻我们应该保证Qt
的主线程保持没有内部事件在运行,等待着响应外部事件):
- 视窗管理活动(键盘按键、鼠标点击,与视窗的交互等等);
- socket活动 (有可见的用来读取的数据或者一个可写的非阻塞Socket, 一个新的Socket连接的产生);
- timers (即计时器开始计时)
- 其它线程Post的事件
为什么需要事件循环?
需要使用事件循环的类
Widgets
绘图与交互: 当派发QPaintEvent
事件时,QWidget::paintEvent()
将会被调用。QPaintEvent
可以产生于内部的QWidget::update()
,也可以产生于外部的视窗管理(比如,一个显示被隐藏的窗口)。同样的,各种各样的交互(键盘、鼠标等)所对应的事件均需要事件循环来派发Timers
:计时器超时后,让Qt
通过返回事件循环让那些调用为你工作。Networking
阻塞事件循环
永远不要阻塞事件循环,假定你有一个按钮button,它被按下时会emit一个信号;还有我们定义了一个Worker对象连接了这个信号,而且这个对象的槽做了很多耗时的事情。当你点击完这个按钮后,从上至下的函数调用栈如下所示:
main(int, char **)
QApplication::exec()
[...]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[...]
Worker::doWork()
在main()
中,我们通过调用QApplication::exec()
。开启了事件循环。视窗管理者发送了鼠标点击事件,该事件被Qt
内核捕获,并转换成QMouseEvent
,随后通过QApplication::notify()
(notify
并没有在上述代码里显示)发送到我们的widget
的event()
方法中。因为Button
并没有重载event()
,它的基类QWidget
方法得以调用。 QWidget::event()
检测出传入的事件是一个鼠标点击,并调用其专有的事件处理器,即Button::mousePressEvent()
。我们重载了 mousePressEvent
方法,并发射了Button::clicked()
信号,该信号激活了我们worker
对象中十分耗时的Worker::doWork()
槽。当worker
对象在繁忙的工作时,事件循环将什么也不做,它分发了鼠标点击事件,并且因等待event handler
返回而被阻塞。我们阻塞了事件循环,也就是说,在我们的doWork()
槽干完活之前再不会有事件派发了,也不会有挂起的事件被处理。当事件派发被就此卡住时,widgets
也将不会再刷新自己(QPaintEvent
对象将在事件队列中静候),也不能有进一步地与widgets
交互的事件发生,计时器也不会再开始计时,网络通讯也将变得迟钝、停滞。更严重的是,许多视窗管理程序会检测到你的应用不再处理事件,从而告诉用户你的程序不再有响应(not responding
). 这就是为什么快速的响应事件并尽可能快的返回事件循环如此重要的原因。
强制事件循环
对于需要长时间运行的任务,我们应该怎么做才会不阻塞事件循环? 一个可行的答案是将这个任务移动另一个线程中;一个可能的方案是,在我们的受阻塞的任务中,通过调用QCoreApplication::processEvents()
人工地强迫事件循环运行;另一个可选的强制地重入事件的方案是使用QEventLoop
类,通过调用QEventLoop::exec()
,我们重入了事件循环,而且我们可以把信号连接到QEventLoop::quit()
槽上使得事件循环退出。
如何强制事件循环
- 将这个任务移动到另一个线程中
- 在我们受阻塞的任务中,通过调用
QCoreApplication::processEvents()
人工强迫事件循环运行 - 通过调用
QEventLoop::exec()
,重入事件循环,而且可以把信号连接到QEventLoop::quit()
槽上使得事件循环退出。
QNetworkAccessManager qnam;
QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(...)));
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
// QNetworkReply没有提供一个阻塞式的API,而且它要求一个事件循环。我们进入到一个局部的,并且当回应完成时,局部事件循环退出
博主记:简单理解了一下在事件循环中不使用线程来做耗时操作,不过目前博主的开发多数都是使用线程,所以更深入的不解释。
Qt线程类
QThread
QThread
是Qt
中一个对线程支持的核心底层类。 每个线程对象代表了一个运行的线程。由于Qt
的跨平台特性,QThread
成功隐藏了所有在不同操作系统里使用线程的平台相关性代码。为了运用QThread
从而让代码在一个线程里运行,我们可以创建一个QThread
的子类,并重载QThread::run()
方法:
class Thread : public QThread {
protected:
void run() {
/* your thread implementation goes here */
}
};
Qt 4.4
版本之后,QThread
不再支持抽象类;现在虚函数QThread::run()
实际上是简单调用了QThread::exec()
,而它启动了线程的事件循环。
- 举例:
class WorkerThread : public QThread
{
Q_OBJECT
void run() Q_DECL_OVERRIDE {
QString result;
emit resultReady(result);
}
signals:
void resultReady(const QString &s);
};
void MyObject::startWorkInAThread()
{
WorkerThread *workerThread = new WorkerThread(this);
connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
workerThread->start(); // 自动调用run()函数
}
在这个例子中,线程中将不会有任何的事件循环运行除非调用exec()
。注意,在一个线程实例位于实例化它的旧线程中,而非调用run()
的新线程中,这意味着所有线程的排队槽将在旧线程中执行。在子类化QThread
时,构造函数在旧线程中执行,而run()
在新线程中执行。
线程与QObjects
线程的事件循环
QThread
对象在它们所代表的线程中开启了新的事件循环。因此,我们说main
事件循环是由调用main()
的线程通过QCoreApplication::exec()
创建的。main线程
也被称为GUI
线程。因为它是界面相关操作的唯一允许的进程。一个QThread
的局部事件循环可以通过调用QThread::exec()
来开启(它包含在run()
方法内部)。
class Thread : public QThread {
protected:
void run() {
/* ... initialize ... */
exec();
}
};
正如我们之前所提到的,自从Qt 4.4
的QThread::run()
方法不再是一个纯虚函数,它调用了QThread::exec()
。就像QCoreApplication
,QThread
也有QThread::quit()
和QThread::exit()
来停止事件循环。
一个线程的事件循环为驻足在该线程中的所有QObjects
派发了所有事件,其中包括在这个线程中创建的所有对象,或者移植到这个线程中的对象。QObject
的依附性(thread affinity
)是指某一个线程,该对象驻足在该线程内。
class MyThread : public QThread
{
public:
MyThread()
{
otherObj = new QObject;
}
private:
QObject obj;
QObject *otherObj;
QScopedPointer<QObject> yetAnotherObj;
};
在我们创建了MyThread
对象后,obj
,otherobj
,yetAnotherObj
是在运行MyThread
构造函数的线程中创建的。因此,这三个对象没有驻足在MyThread
线程中,而是驻足在创建MyThread
实例的线程中。
注意:在QCoreApplication
对象之前创建的QObjects
没有依附于某一个线程。因此,没有人会为它们做事件派发处理(换句话说,QCoreApplication
构建了代表主线程的QThread
对象)
使用线程安全的QCoreApplication::postEvent()方
法来为某个对象分发事件。它将把事件加入到对象所驻足的线程事件队列中。因此,除非事件对象依附的线程有一个正在运行的事件循环,否则事件不会被派发。
理解QObject
和它所有的子类不是线程安全的(尽管可重入);因此,除非你序列化对象内部数据所有可访问的接口、数据,否则你不能让多个线程同一时刻访问相同的QObject
(比如,加锁)。
注意:尽管你可以另一个线程访问对象,但是该对象此时可能正在处理它所驻足的线程事件循环派发给它的事件!基于这种原因,你不能从另一个线程中删除一个QObject
,一定要使用QObject::deleteLater()
,它会Post
一个事件,目标删除对象最终会在它所生存的线程中被删除。(QObject::deleteLater
的作用是当控制流回到该对象所依附的线程事件循环时,该对象才会被”本”线程删除)。
比如:
void Widget::startNetStreamThread()
{
netThread = new QThread();
netStrThread = new netStreamThread();
netStrThread->moveToThread(netThread);
connect(netThread, &QThread::finished, netThread, &QObject::deleteLater);
connect(netThread, &QThread::finished, netStrThread, &QObject::deleteLater);
}
其中netStrThread
是通过继承QObject
创建的线程,使用moveToThread
的方法放置到netThread
中运行。而netThread
对象和netStrThread
对象都是在主线程,也就是widget
线程中创建的,所以最好不要在主线程中直接删除netThread
线程,而要等到netThread
线程执行完成,回到netThread
线程的事件循环的时候,在进行删除,从而避免出错。同时netStrThread
线程是在netThread
线程中运行的,所以可以等到回到netStrThread
线程中时来删除,从而避免出现错误。
我应该在什么时候使用线程
当你不得不使用一个阻塞式API时
当你需要(通过信号和槽,或者是事件、回调函数)使用一个没有提供非阻塞式API的库或者代码时,为了阻止冻结事件循环的唯一可行的解决方案是开启一个进程或者线程。由于创建一个新的进程的开销显然要比开启一个线程的开销大,后者往往是最常见的一种选择。
博主记:也就是说,如果在主线程中发生了阻塞,这时候事件循环就会停止响应。
当你向扩展至多核
多线程允许你的程序利用多核系统的优势。因为每个线程都是被操作系统独立调度的,因此如何你的应用运行在这样多核机器上,调度器很可能同时在不同的处理器上运行每个线程。