Qt 学习之路:线程和事件循环
前面一章我们简单介绍了如何使用QThread
实现线程。现在我们开始详细介绍如何“正确”编写多线程程序。我们这里的大部分内容来自于Qt的一篇Wiki文档,有兴趣的童鞋可以去看原文。
在介绍在以前,我们要认识两个术语:
- 可重入的(Reentrant):如果多个线程可以在同一时刻调用一个类的所有函数,并且保证每一次函数调用都引用一个唯一的数据,就称这个类是可重入的(Reentrant means that all the functions in the referenced class can be called simultaneously by multiple threads, provided that each invocation of the functions reference unique data.)。大多数 C++ 类都是可重入的。类似的,一个函数被称为可重入的,如果该函数允许多个线程在同一时刻调用,而每一次的调用都只能使用其独有的数据。全局变量就不是函数独有的数据,而是共享的。换句话说,这意味着类或者函数的使用者必须使用某种额外的机制(比如锁)来控制对对象的实例或共享数据的序列化访问。
- 线程安全(Thread-safe):如果多个线程可以在同一时刻调用一个类的所有函数,即使每一次函数调用都引用一个共享的数据,就说这个类是线程安全的(Threadsafe means that all the functions in the referenced class can be called simultaneously by multiple threads even when each invocation references shared data.)。如果多个线程可以在同一时刻访问函数的共享数据,就称这个函数是线程安全的。
进一步说,对于一个类,如果不同的实例可以被不同线程同时使用而不受影响,就说这个类是可重入的;如果这个类的所有成员函数都可以被不同线程同时调用而不受影响,即使这些调用针对同一个对象,那么我们就说这个类是线程安全的。由此可以看出,线程安全的语义要强于可重入。接下来,我们从事件开始讨论。之前我们说过,Qt 是事件驱动的。在 Qt 中,事件由一个普通对象表示(QEvent
或其子类)。这是事件与信号的一个很大区别:事件总是由某一种类型的对象表示,针对某一个特殊的对象,而信号则没有这种目标对象。所有QObject
的子类都可以通过覆盖QObject::event()
函数来控制事件的对象。
事件可以由程序生成,也可以在程序外部生成。例如:
QKeyEvent
和QMouseEvent
对象表示键盘或鼠标的交互,通常由系统的窗口管理器产生;QTimerEvent
事件在定时器超时时发送给一个QObject
,定时器事件通常由操作系统发出;QChildEvent
在增加或删除子对象时发送给一个QObject
,这是由 Qt 应用程序自己发出的。
需要注意的是,与信号不同,事件并不是一产生就被分发。事件产生之后被加入到一个队列中(这里的队列含义同数据结构中的概念,先进先出),该队列即被称为事件队列。事件分发器遍历事件队列,如果发现事件队列中有事件,那么就把这个事件发送给它的目标对象。这个循环被称作事件循环。事件循环的伪代码描述大致如下所示:
C++
1
2
3
4
5
6
7
|
while (is_active)
{
while (!event_queue_is_empty) {
dispatch_next_event();
}
wait_for_more_events();
}
|
正如前面所说的,调用QCoreApplication::exec()
函数意味着进入了主循环。我们把事件循环理解为一个无限循环,直到QCoreApplication::exit()
或者QCoreApplication::quit()
被调用,事件循环才真正退出。
伪代码里面的while
会遍历整个事件队列,发送从队列中找到的事件;wait_for_more_events()
函数则会阻塞事件循环,直到又有新的事件产生。我们仔细考虑这段代码,在wait_for_more_events()
函数所得到的新的事件都应该是由程序外部产生的。因为所有内部事件都应该在事件队列中处理完毕了。因此,我们说事件循环在wait_for_more_events()
函数进入休眠,并且可以被下面几种情况唤醒:
- 窗口管理器的动作(键盘、鼠标按键按下、与窗口交互等);
- 套接字动作(网络传来可读的数据,或者是套接字非阻塞写等);
- 定时器;
- 由其它线程发出的事件(我们会在后文详细解释这种情况)。
在类 UNIX 系统中,窗口管理器(比如 X11)会通过套接字(Unix Domain 或 TCP/IP)向应用程序发出窗口活动的通知,因为客户端就是通过这种机制与 X 服务器交互的。如果我们决定要实现基于内部的socketpair(2)
函数的跨线程事件的派发,那么窗口的管理活动需要唤醒的是:
- 套接字 socket
- 定时器 timer
这也正是select(2)
系统调用所做的:它监视窗口活动的一组描述符,如果在一定时间内没有活动,它会发出超时消息(这种超时是可配置的)。Qt 所要做的,就是把select()
的返回值转换成一个合适的QEvent
子类的对象,然后将其放入事件队列。好了,现在你已经知道事件循环的内部机制了。
至于为什么需要事件循环,我们可以简单列出一个清单:
- 组件的绘制与交互:
QWidget::paintEvent()
会在发出QPaintEvent
事件时被调用。该事件可以通过内部QWidget::update()
调用或者窗口管理器(例如显示一个隐藏的窗口)发出。所有交互事件(键盘、鼠标)也是类似的:这些事件都要求有一个事件循环才能发出。 - 定时器:长话短说,它们会在
select(2)
或其他类似的调用超时时被发出,因此你需要允许 Qt 通过返回事件循环来实现这些调用。 - 网络:所有低级网络类(
QTcpSocket
、QUdpSocket
以及QTcpServer
等)都是异步的。当你调用read()
函数时,它们仅仅返回已可用的数据;当你调用write()
函数时,它们仅仅将写入列入计划列表稍后执行。只有返回事件循环的时候,真正的读写才会执行。注意,这些类也有同步函数(以waitFor
开头的函数),但是它们并不推荐使用,就是因为它们会阻塞事件循环。高级的类,例如QNetworkAccessManager
则根本不提供同步 API,因此必须要求事件循环。
有了事件循环,你就会想怎样阻塞它。阻塞它的理由可能有很多,例如我就想让QNetworkAccessManager
同步执行。在解释为什么永远不要阻塞事件循环之前,我们要了解究竟什么是“阻塞”。假设我们有一个按钮Button
,这个按钮在点击时会发出一个信号。这个信号会与一个Worker
对象连接,这个Worker
对象会执行很耗时的操作。当点击了按钮之后,我们观察从上到下的函数调用堆栈:
1
2
3
4
5
6
7
8
|
main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork()
|
我们在main()
函数开始事件循环,也就是常见的QApplication::exec()
函数。窗口管理器侦测到鼠标点击后,Qt 会发现并将其转换成QMouseEvent
事件,发送给组件的event()
函数。这一过程是通过QApplication::notify()
函数实现的。注意我们的按钮并没有覆盖event()
函数,因此其父类的实现将被执行,也就是QWidget::event()
函数。这个函数发现这个事件是一个鼠标点击事件,于是调用了对应的事件处理函数,就是Button::mousePressEvent()
函数。我们重写了这个函数,发出Button::clicked()
信号,而正是这个信号会调用Worker::doWork()
槽函数。有关这一机制我们在前面的事件部分曾有阐述,如果不明白这部分机制,请参考前面的章节。
在worker
努力工作的时候,事件循环在干什么?或许你已经猜到了答案:什么都没做!事件循环发出了鼠标按下的事件,然后等着事件处理函数返回。此时,它一直是阻塞的,直到Worker::doWork()
函数结束。注意,我们使用了“阻塞”一词,也就是说,所谓阻塞事件循环,意思是没有事件被派发处理。
在事件就此卡住时,组件也不会更新自身(因为QPaintEvent
对象还在队列中),也不会有其它什么交互发生(还是同样的原因),定时器也不会超时并且网络交互会越来越慢直到停止。也就是说,前面我们大费周折分析的各种依赖事件循环的活动都会停止。这时候,需要窗口管理器会检测到你的应用程序不再处理任何事件,于是告诉用户你的程序失去响应。这就是为什么我们需要快速地处理事件,并且尽可能快地返回事件循环。
现在,重点来了:我们不可能避免业务逻辑中的耗时操作,那么怎样做才能既可以执行那些耗时的操作,又不会阻塞事件循环呢?一般会有三种解决方案:第一,我们将任务移到另外的线程(正如我们上一章看到的那样,不过现在我们暂时略过这部分内容);第二,我们手动强制运行事件循环。想要强制运行事件循环,我们需要在耗时的任务中一遍遍地调用QCoreApplication::processEvents()
函数。QCoreApplication::processEvents()
函数会发出事件队列中的所有事件,并且立即返回到调用者。仔细想一下,我们在这里所做的,就是模拟了一个事件循环。
另外一种解决方案我们在前面的章节提到过:使用QEventLoop
类重新进入新的事件循环。通过调用QEventLoop::exec()
函数,我们重新进入新的事件循环,给QEventLoop::quit()
槽函数发送信号则退出这个事件循环。拿前面的例子来说:
C++
1
2
3
4
5
6
|
QEventLoop eventLoop;
connect(netWorker, &NetWorker::finished,
&eventLoop, &QEventLoop::quit);
QNetworkReply *reply = netWorker->get(url);
replyMap.insert(reply, FetchWeatherInfo);
eventLoop.exec();
|
QNetworkReply
没有提供阻塞式 API,并且要求有一个事件循环。我们通过一个局部的QEventLoop
来达到这一目的:当网络响应完成时,这个局部的事件循环也会退出。
前面我们也强调过:通过“其它的入口”进入事件循环要特别小心:因为它会导致递归调用!现在我们可以看看为什么会导致递归调用了。回过头来看看按钮的例子。当我们在Worker::doWork()
槽函数中调用了QCoreApplication::processEvents()
函数时,用户再次点击按钮,槽函数Worker::doWork()又
一次被调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork() // <strong>第一次调用</strong>
QCoreApplication::processEvents() // <strong>手动发出所有事件</strong>
[…]
QWidget::event(QEvent * ) // <strong>用户又点击了一下按钮…</strong>
Button::mousePressEvent(QMouseEvent *)
Button::clicked() // <strong>又发出了信号…</strong>
[…]
Worker::doWork() // <strong>递归进入了槽函数!</strong>
|
当然,这种情况也有解决的办法:我们可以在调用QCoreApplication::processEvents()
函数时传入QEventLoop::ExcludeUserInputEvents
参数,意思是不要再次派发用户输入事件(这些事件仍旧会保留在事件队列中)。
幸运的是,在删除事件(也就是由QObject::deleteLater()
函数加入到事件队列中的事件)中,没有这个问题。这是因为删除事件是由另外的机制处理的。删除事件只有在事件循环有比较小的“嵌套”的情况下才会被处理,而不是调用了deleteLater()
函数的那个循环。例如:
C++
1
2
3
4
|
QObject *object = new QObject;
object->deleteLater();
QDialog dialog;
dialog.exec();
|
这段代码并不会造成野指针(注意,QDialog::exec()
的调用是嵌套在deleteLater()
调用所在的事件循环之内的)。通过QEventLoop
进入局部事件循环也是类似的。在 Qt 4.7.3 中,唯一的例外是,在没有事件循环的情况下直接调用deleteLater()
函数,那么,之后第一个进入的事件循环会获取这个事件,然后直接将这个对象删除。不过这也是合理的,因为 Qt 本来不知道会执行删除操作的那个“外部的”事件循环,所以第一个事件循环就会直接删除对象。
Qt 学习之路:线程和事件循环的更多相关文章
- Qt 学习之路:自定义事件
尽管 Qt 已经提供了很多事件,但对于更加千变万化的需求来说,有限的事件都是不够的.例如,我要支持一种新的设备,这个设备提供一种崭新的交互方式,那么,这种事件如何处理呢?所以,允许创建自己的事件 类型 ...
- Qt 学习之路 2(72):线程和事件循环
Qt 学习之路 2(72):线程和事件循环 <理解不清晰,不透彻> -- 有需求的话还需要进行专题学习 豆子 2013年11月24日 Qt 学习之路 2 34条评论 前面一章我 ...
- Qt 的线程与事件循环——可打印threadid进行观察槽函数到底是在哪个线程里执行,学习moveToThread的使用)
周末天冷,索性把电脑抱到床上上网,这几天看了 dbzhang800 博客关于 Qt 事件循环的几篇 Blog,发现自己对 Qt 的事件循环有不少误解.从来只看到现象,这次借 dbzhang800 的博 ...
- Qt 学习之路 2(74):线程和 QObject
Home / Qt 学习之路 2 / Qt 学习之路 2(74):线程和 QObject Qt 学习之路 2(74):线程和 QObject 豆子 2013年12月3日 Qt 学习之路 2 2 ...
- Qt 学习之路 2(73):Qt 线程相关类
Home / Qt 学习之路 2 / Qt 学习之路 2(73):Qt 线程相关类 Qt 学习之路 2(73):Qt 线程相关类 豆子 2013年11月26日 Qt 学习之路 2 7条评论 希 ...
- Qt 学习之路 2(23):自定义事件
Qt 学习之路 2(23):自定义事件 豆子 2012年10月23日 Qt 学习之路 2 21条评论 尽管 Qt 已经提供了很多事件,但对于更加千变万化的需求来说,有限的事件都是不够的.例如, ...
- Qt 学习之路 2(71):线程简介
Qt 学习之路 2(71):线程简介 豆子 2013年11月18日 Qt 学习之路 2 30条评论 前面我们讨论了有关进程以及进程间通讯的相关问题,现在我们开始讨论线程.事实上,现代的程序中,使用线程 ...
- Qt 学习之路 2(22):事件总结
Qt 学习之路 2(22):事件总结 豆子 2012年10月16日 Qt 学习之路 2 47条评论 Qt 的事件是整个 Qt 框架的核心机制之一,也比较复杂.说它复杂,更多是因为它涉及到的函数众多,而 ...
- Qt 学习之路 2(18):事件
Home / Qt 学习之路 2 / Qt 学习之路 2(18):事件 Qt 学习之路 2(18):事件 豆子 2012年9月27日 Qt 学习之路 2 60条评论 事件(event)是由系统 ...
- Qt 学习之路:线程总结
前面我们已经详细介绍过有关线程的一些值得注意的事项.现在我们开始对线程做一些总结. 有关线程,你可以做的是: 在QThread子类添加信号.这是绝对安全的,并且也是正确的(前面我们已经详细介绍过,发送 ...
随机推荐
- javascript 函数声明问题
(function(){ //运行正常 test1(); function test1() { console.log('123'); }; })() (function(){ //出错,test2未 ...
- js 表达式与运算符 详解(下)
比较运算符: > .>= .<. <=. ==. !=. ===. !==. 比较运算符的结果都为布尔值 ==只比较值是否相等 而 ===比较的是值和数据类型都要 ...
- Ubuntu phpmyadmin 缺少mcrypt扩展解决方法
之前在登陆phpmyadmin的时候,会出现警告说缺少mcrypt扩展的错误,一直没去解决这个问题,觉得没什么影响就算了. 今天谷歌了一下,原来是php5没有启用mcrypt模块. sudo ph ...
- apt-cache madison package-name
apt-cache madison package-name 搜索软件有那些可用版本,
- [r]Setting up Django and your web server with uWSGI and nginx
Setting up Django and your web server with uWSGI and nginx This tutorial is aimed at the Django user ...
- android的padding和margin的区别
android:padding和android:layout_margin的区别:padding是站在父view的角度描述问题,它规定它里面的内容必须与这个父view边界的距离. margin则是站在 ...
- 一篇旧文章,结合汇编探索this指针
//VC6.0下成功编译 #include <iostream.h> class X{ public: void foo(int b,int c){ this->a=b*c; cou ...
- 了解运行时类型信息(RTTI)
RTTI需要引用单元TypeInfo GetPropInfo 函数用于获得属性的 RTTI 指针 PPropInfo.它有四种重载形式,后面三种重载的实现都是调用第一种形式.AKinds 参数用于限制 ...
- PGA突破pga_aggregate_target限制
SQL> show parameter pga NAME TYPE VALUE ------------------------------------ ----------- ...
- 【转】Jollen 的 Android 教學,#12: 如何建立選單 Menu
原文网址:http://www.jollen.org/blog/2009/06/jollen-android-programming-12.html Android應用程式的UI可以使用XML來定義, ...