http://www.xue5.com/Developer/Software/608233.html
english: http://qt-project.org/wiki/Threads_Events_QObjects
在Qt频道讨论最多的话题之一就是线程,许多人在这频道中询问该怎么解决在不同线程中运行的代码问题。
粗看他们的代码,问题十有八九是第一次使用线程,而他们落入了并行编程的陷阱中了。
对在Qt中创建和运行线程和编程风格的缺失和用其它语言和工具的习惯,经常导致人们搬起石头砸自己的脚。同时,线程支持在Qt中是一把双刃剑:它既使你能更加简单的加入多线程,但是它也增加了许多你必须要关注的特性。
本文的目的不在于教你怎么使用线程,做适当的锁,开发并行程序,也不是写可扩展性的程序,现在有许多好书会教你这些问题。在这里,可以看看Doc频道的文章。本文的目的是引导和介绍使用者理解线程,以便于避免落入常见的陷阱中,帮助他们开发有着更健壮框架的代码。
预备知识:
不是一开始就介绍多线程编程,我们希望你有着这些基础:
C++基础;
Qt基础:QObject信号和槽,事件处理
线程是什么,线程间的关系是什么,操作系统是怎么处理它们的;
如何启动和停止一个线程,或等到它停止在主要的操作系统环境下;
如何使用Mutex,semaphores和等待条件去创建线程安全和可重入的函数,数据结构与类。
在此文中我们将参照这些术语:
可重入:
一个可重入的类代表它的实例可以被多个线程使用,在同时访问时提供最多一个线程的权限。一个函数可重入代表它可以同时被多个线程调用,每个函数访问到的数据都是特定的。
线程安全:
一个线程安全的类代表能在多个线程中同时使用它的实例,一个线程安全的函数代表能被多个线程中同时调用即是存在着共享数据的引用。
事件和事件循环
作为一个事件驱动的工具包,事件和事件传递扮演者Qt架构中的中心角色。在本文中我们不会给出一个对这个话题的全面的概述,我们将着眼于一些线程相关的概念。
事件能被程序的内部和外部产生,举个例子:
QKeyEvent和QMouseEvent对象代表了一个键盘和鼠标的事件,它们从窗口由用户的操作而产生。
QTimerEvent对象是当某个时间被激发时投入,它们都由操作系统产生。
QChildEvent对象是当一个子窗口被添加或者移除时候被送入QObject的,它们的源头是Qt程序自己。
事件的重点是它们被产生的时候不会被传递;它们会先进入事件队列,某刻会被传递。传送者自己循环访问事件队列并把事件传递给目标QObject对象,因此称作事件循环。概念上说,时间循环就像这个:
1. while (is_active)
2. {
3. while (!event_queue_is_empty)
4. dispatch_next_event();
5.
6. wait_for_more_events();
7. }
我们通过运行QCoreApplication::exec()来进入消息循环,这个循环直到exit()或者quit()被调用时才会被堵塞,然后退出。
这个”wait_for_more_events()”函数处于堵塞状态,直到有新的事件被产生了。假如我们考虑它,所有在此刻可能产生的事件是外部源头的。因此,这个消息循环能被以下情况唤醒:
窗口管理活动(鼠标按键操作等);
套接字事件;
定时器事件;
其它线程中投递的事件、
在Unix-like系统中,窗口管理器通过套接字来通知应用程序,即使客户端使用它们来与x server通讯。如果我们决定用内部的套接字对去实现跨线程的事件投递,所有剩下的唤醒条件如下:
套接字;
定时器;
这个就是select(2)系统调用所做的;它监视着一系列的一系列的活动者的描述符,如果它们在一定的时间内没有特定的活动,它们就超时了。
一个运行着的事件循环需要什么?
这个不是完整的列表,但是如果你有整体画面,你将能够去猜测什么类需要一个运行着的事件循环。
Widgets的绘画和互动:QWidget::paintEvent()将在传递QPaintEvent对象时候被调用,这个对象将会在调用QWidget::update()或者窗口管理器的时候产生:响应的事件将需要一个时间循环去分发。
Timers:长话短说,它们在当select(2)或者超时的时候产生,因此它们需要让Qt在返回时间循环的时候作这些调用。
Networking“所有的底层Qt网络类(QTcpSocket,QUdpSocket,QTcpServer等)都是异步设计的。当你调用ready(),它们只是返回已经可用的数据,当你调用write(),它们只是将这个操作放入队列,适时会写入。只有当你返回消息循环的时候真实的读取,写入才会执行。注意它们确实提供了同步的方法,但是它们的用法是不被提倡的,因为它们会堵塞事件循环。高级类,比如QNetworkAccessManager,简单的不提供同步API,需要一个事件循环。
堵塞事件循环
在我们讨论为什么你应该从不堵塞消息循环之前,我们试着分析堵塞的含义。假象你有一个按钮,它将会在它被点击的时候发出clicked信号;在我们的对象中连接着一个槽函数,当你点击了那个按钮后,栈追踪将会像这样:
1. main(int, char **)
2. QApplication::exec()
3. […]
4. QWidget::event(QEvent *)
5. Button::mousePressEvent(QMouseEvent *)
6. Button::clicked()
7. […]
8. Worker::doWork()
在main函数中我们启动了时间循环,平常的调用了exex(),窗口管理器给我们发送了一个鼠标点击事件,它被Qt内核取走,转换成QMouseEvent并被送往我们widget的event()方法,该方法被QApplication::notify()发送。因为按钮没有重写event(),基类方法将被调用,QWidget::event()检测到了这个事件确实是一个鼠标点击事件,然后调用特定的事件处理函数,那就是Button::mousePressEvent(),我们重写这个方法去发送clicked()信号,这将会调用被连接的槽函数。
当该对象处理量很大,那么消息循环在作什么?我们应该猜测它:什么都不做!它分发鼠标按下事件,然后就堵塞着等待着事件处理函数返回。这个就是堵塞了时间循环,它意味着没有消息被分发了,知道我们从槽函数返回了,然后继续处理挂起的消息。
在消息循环被卡住的情况下,widgets将不能更新它们自身,不可能有更多的互动,timers将不会被激发,网络通讯将缓慢下来,或者停止。进一步的说,许多窗口管理器将检测到你的应用程序不在处理事件了,然后告诉用户你的程序没有响应。这就是为什么快速的对事件响应并且即时返回到事件循环是多么的重要!
强制事件分发
所以,假如我们有一个很长的任务去运行但是又不希望堵塞这个消息循环,该怎么做呢?一个可能的回答是将这个任务移到另一个线程中,在下一个张洁我们将看到这是如何做的。我们也能手动强制事件循环去运行,这个方法是通过在堵塞的任务函数中调用QCoreApplication::processEvent()来实现的,QCoreApplication::processEvent()将处理所有在消息队列中的消息并返回给调用者。
另一个可选的选项是我们能够强制重入事件循环的对象,就是QEventLoop类。通过调用QEventLoop::exec()我们将重入事件循环,然后我们能将槽函数QVentLoop::quit()连接到信号上去使它退出。举个例子:
1. QNetworkAccessManager qnam;
2. QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(...)));
3. QEventLoop loop;
4. QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
5. loop.exec();
6. /* reply has finished, use it */
QNetworkReply不提供堵塞的API,它要求一个在运行的事件循环。我们进入了一个本地的事件循环,然后当回复完成时候,这个本地的循环退出了。
要特别小心的是在其它路径下重入事件循环:它可能导致不希望的递归!让我们回到前面看看按钮的例子。假如我们在槽函数中调用了QCoreApplication::processEvent(),当用户点击了这个按钮,这个槽函数将被再次调用:
1. main(int, char **)
2. QApplication::exec()
3. […]
4. QWidget::event(QEvent *)
5. Button::mousePressEvent(QMouseEvent *)
6. Button::clicked()
7. […]
8. Worker::doWork() // first, inner invocation
9. QCoreApplication::processEvents() // we manually dispatch events and…
10. […]
11. QWidget::event(QEvent * ) // another mouse click is sent to the Button…
12. Button::mousePressEvent(QMouseEvent *)
13. Button::clicked() // which emits clicked() again…
14. […]
15. Worker::doWork() // DANG! we’ve recursed into our slot.
一个快速并简便的变通方法是把QEventLoop::ExcludeUserInputEvent传递给QCoreApplication::processEvents(),这会告诉消息循环不要再次分发任何用户的输入事件。
幸运的是,这个相同的事情不会在检测事件中发生。事实上,它们被Qt通过特殊的方法处理了,只有当运行的时间循环有了一个比deleteLater被调用后更小的”nesting”值才会被处理:
1. QObject *object = new QObject;
2. object->deleteLater();
3. QDialog dialog;
4. dialog.exec();
将不会使object成为一个悬空指针。相同的东西被应用到了本地的事件循环中。唯一的一个显著区别我已经发现了,它在假如当没有事件循环在运行的时候deleteLater被调用了的条件下,然后第一个消息循环进入了后会取走这个事件,然后删除这个object。这是相当合理的,因为Qt不知道任何外部的循环将最终影响这个检测,因此马上删除了这个object。
Qt 线程类
Qt对线程的支持已经好几年了,4.0版本已经默认支持所有支持的平台。Qt现在提供几个相关的线程处理类;让我们开始大体看一下。
QThread
QThread是一个核心,底层的对线程支持的类。一个QThread对象代表了一个线程的执行。因为Qt跨平台的特性,QThread设法隐藏所有平台使用线程的特定的代码。
为了去使用一个在另一个线程中运行代码的线程对象,我们继承它,然后重写QThread::run()方法:
1. class Thread : public QThread {
2. protected:
3. void run() {
4. /* your thread implementation goes here */
5. }
6. };
然后我们能这样用
1. Thread *t = new Thread;
2. t->start(); // start(), not run()!
QRunnable和QThreadPool
QRunnable是一个轻量级的抽象类,它能在另一个线程中运行代码,是一种运行然后遗忘的风格。为了这样做,我们应该去继承它,然后实现它的run()纯虚方法:
1. class Task : public QRunnable {
2. public:
3. void run() {
4. /* your runnable implementation goes here */
5. }
6. };
为了真实的运行一个QRunnable对象,我们使用QThreadPool类,它管理了线程池。通过调用QThreadPool::start(runnable)我们把一个QRunnable对象放入了一个线程池队列。只要线程可用,QRunnable将会挑选出它并且运行它。所有的Qt应用程序有一个全局的可用的线程池,该池通过调用QThreadPool::globalInstance()来调用获得。
请注意,QRunnable不是一个QObject对象,没有内置的显式的通讯方法;你必须自己编码,使用低级的线程基本元素。
QtConcurrent
Qtconcurrent是一个高层次的API,建立在QThreadPool上层,对大部分普通的并行计算都能很好的处理:map,reduce,filter;它也提供了一个QtConcurrent::run()方法来简单的把一个函数运行在另一个线程。
不像QThread和QRunnable,Qtconcurrent不要求我们使用低级同步元素:所有的QtConcurrent方法返回了一个QFuture对象,该对象能查询一些状态信息,去暂停,挂起,取消这个计算,同时包含了这个结果。QFutureWather类能被用来监视QFuture进程,然后通过信号和槽进行互动。
Threads和QObject
每一个线程的事件循环
到目前为止我们已经经常谈到“时间循环”,就算如此在Qt程序中也只有一个消息循环。情况不是这样的:QThread对象能够启动一个基于线程的运行在它们代表的线程中的事件循环。因此,我们称为主事件循环的是由main()启动的线程,并且由QCoreApplication::exec()执行的线程。它也称之为GUI线程,因为它是唯一一个允许GUI相关操作的线程。一个QThread本地事件循环能够通过调用QThread::exec()来调用:
1. class Thread : public QThread {
2. protected:
3. void run() {
4. /* ... initialize ... */
5.
6. exec();
7. }
8. };
正如我们前面提到的,自从Qt 4.4 开始QThread::run()不再是一个纯虚方法,代替的是通过调用QThread::exec()。就像QCoreApplication,QThread也有QThread::quit()和QThread::exit()方法去停止这个事件循环。
一个线程的事件循环给所有的存在于那个线程的QObject来分发事件。这个包括默认的所有objects是被创建入那个线程的,或者被移动到那个线程的。我们也称之为一个QObject和一个线程的线程关联度,意味着那个对象是存在于那个线程中的。这个适用于在QThread对象中运行了构造函数的对象。
1. class MyThread : public QThread
2. {
3. public:
4. MyThread()
5. {
6. otherObj = new QObject;
7. }
8.
9. private:
10. QObject obj;
11. QObject *otherObj;
12. QScopedPointer<QObject> yetAnotherObj;
13. };
Obj,otherObj,yetAnotherObj在我们创建了MyThread对象后的线程关联度是什么?我们必须看下创建它们的线程:它是运行MyThread构造函数的那个线程。因此,所有的三个对象不是存在于MyThread线程中,只是在这个线程中创建了一个MyThread实例。
我们能在任何情况下通过调用QObject::thread()函数来查询线程关联度。注意QObject在QCoreApplication对象之前创建的话就没有进程关联度,因此没有事件能被分发给它们。
我们也能使用线程安全的QCoreApplication::postEvent()方法来传递一个事件给特定的对象。这个将进入这个对象所处线程的线程事件队列;因此,这个事件只有当线程有一个运行中的消息循环才能被分发。
理解QObject和它子类不是线程安全是非常重要的;因此,你不能同时从多个线程访问QObject,除非你序列化了所有对对象内部数据的访问。记住这个对象在当你从另一个线程中访问它时可能正在处理它所在线程的事件!同样的原因,你不能在另一个线程中删除一个QObject,你必须用QObject::deleteLater(),它会投递给这个对象所处的线程一个事件,它会最终删除掉这个QObject。
此外,QWidget和它所有的子类,和其它GUI相关的类不是可重入的:它们只能在GUI线程中使用。
我们能够通过调用QObject::moveToThread()函数来改变QObject的线程关联性;这将会改变它和它子对象的线程关联性。由于QObject不是线程安全的,我们必须在它存在的线程中访问它;这就是说,你只能仅仅把一个线程从它们存在的线程置换入一个新的线程,而不能把它们从另外的线程中恢复过来。进一步说,Qt要求QObject的子对象必须存在于相同的线程中,就是QObject所存在的线程。这个暗示了:
你不能对一个有父对象的对象调用moveToThread()函数;
你不能在QThread中创建一个以QThread对象为父对象的对象;
1. class Thread : public QThread {
2. void run() {
3. QObject *obj = new QObject(this); // WRONG!!!
4. }
5. };
这是因为QThread 对象是存在于另一个线程的,也就是说,对象存在于它被创建的线程。
Qt也要求所有存在于一个线程中的对象在QThread对象被销毁前销毁;这个能很简单的就完成了,只要在QThread()方法中创建栈对象即可。
跨线程的信号和槽
以上述的内容为前提,我们怎么调用存在于另一个线程的对象的方法?Qt提供了一个很漂亮也很简洁的方法:我们在那个线程的事件队列中投递一个消息,然后那个事件的处理函数将会调用我们想要调用的那个方法。这个功能由moc内部实现;因此,只有被Q_INVOKABLE宏修饰的信号,槽和方法才能被另外一个线程调用。
QMetaObject::invokeMethod()静态函数为我们做了所有的工作:
1. QMetaObject::invokeMethod(object, "methodName",
2. Qt::QueuedConnection,
3. Q_ARG(type1, arg1),
4. Q_ARG(type2, arg2));
注意因为参数需要拷贝到背后建立的事件中,它们的类型需要有构造函数来提供,一个析构函数和一个拷贝构造函数,然后必须通过调用qRegisterMetaType()函数注册到Qt的类型系统.
跨线程的信号和槽也是同样的方法。当我们连接一个槽到一个信号的时候,第五个参数被用来区分连接类型。
一个direct connection意味着槽函数一直是直接在信号被发出的那个线程中调用的。
一个queued connection意味着一个事件被投递到接受者所在的线程中,它会被事件循环取出,这会导致槽函数的执行稍晚一点儿。
一个block queued connection类似于queued connection,但是发送者线程被堵塞了,知道事件被接受者所处线程的事件循环给取出后,并且调用完返回才恢复。
一个automatic connection意味着如果线程的接受者与发送者处于同一个线程,那么direct connection被使用;反之queued connection被调用。
在任何情况下,考虑发送信号的对象存在于哪个线程一点儿也不重要。在automatic connection中,Qt检测发送信号的线程,并与接受者所处的线程相比决定哪种连接方式被使用。特别的,current Qt 文档当它表述这个的时候是错误的:
Auto Connection (default) The behavior is the same as the Direct Connection, if the emitter and receiver are in the same thread. The behavior is the same as the Queued Connection, if the emitter and receiver are in different threads.
因为发送对象的所属线程并不一样,举个例子:
1. class Thread : public QThread
2. {
3. Q_OBJECT
4.
5. signals:
6. void aSignal();
7.
8. protected:
9. void run() {
10. emit aSignal();
11. }
12. };
13.
14. /* ... */
15. Thread thread;
16. Object obj;
17. QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot()));
18. thread.start();
aSignal()信号将被新线程发送;由于它被发送时候不是object所处的线程,queued connection将被使用。
另一个常见的陷阱如下:
1. class Thread : public QThread
2. {
3. Q_OBJECT
4.
5. slots:
6. void aSlot() {
7. /* ... */
8. }
9.
10. protected:
11. void run() {
12. /* ... */
13. }
14. };
15.
16. /* ... */
17. Thread thread;
18. Object obj;
19. QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot()));
20. thread.start();
21. obj.emitSignal();
当obj发送asignal()信号,哪种连接方式会被使用?你应该会猜:direct connection。这是因为线程对象存在于发送信号的线程。在aSlot()槽函数中我们可以在它们被run()访问一些Thread的成员变量的时候访问,这些行为是同时发生的:这个导致了灾难。
还有另一个自理,可能是最重要的一个:
1. class Thread : public QThread
2. {
3. Q_OBJECT
4.
5. slots:
6. void aSlot() {
7. /* ... */
8. }
9.
10. protected:
11. void run() {
12. QObject *obj = new Object;
13. connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot()));
14. /* ... */
15. }
16. };
在这种情况下queued connection被使用,因此你需要在这个object存在的线程中使用一个事件循环。
在论坛中你将经常找到一个解决方案,就是添加moveToThread(this)函数到线程的构造函数中。
1. class Thread : public QThread {
2. Q_OBJECT
3. public:
4. Thread() {
5. moveToThread(this); // WRONG
6. }
7.
8. /* ... */
9. };
这个是可以工作的,但这是一个相当糟糕的设计。我们错误的理解了这个线程对象:QThread对象不是线程;它们是围绕着线程的控制对象,因此意味着从另一个线程中被使用。
一个实现相同效果的好的方法是从控制部分分理出工作部分,也就是说,写一个QObject子类,使用moveToThread()去改变线程关联性。
1. class Worker : public QObject
2. {
3. Q_OBJECT
4.
5. public slots:
6. void doWork() {
7. /* ... */
8. }
9. };
10.
11. /* ... */
12. QThread *thread = new QThread;
13. Worker *worker = new Worker;
14. connect(obj, SIGNAL(workReady()), worker, SLOT(doWork()));
15. worker->moveToThread(thread);
16. thread->start();
要做的和不要做的:
你可以。。。
向QThread子类添加信号,这是绝对安全,并且它们会正常工作。
你不应该。。。
使用moveToThread(this)
强制连接类型:这经常意味着你做了些错误的事情,就像混淆了程序逻辑的QThread的控制接口
向QThread子类添加了槽函数:它们将被错误的线程调用,也就是说不是你想要被调用的那个线程,而是object所在的线程,迫使你指定direct connection或者使用moveToThread(this)。
使用QThread::terminate。
你一定不要。。。
在你线程仍在运行的时候退出了你的程序。使用QThread::wait去等待它们的终止。
当你的线程仍在运行的时候摧毁了你的线程。如果你想要集中自我摧毁方法,你能把deleteLater()槽函数连接到finished()信号中。
何时该用线程?
当你必须得用一个堵塞的API
如果你需要没有提供非堵塞API的库或者其它代码,唯一可行的方法去试着避免事件循环被冻结的方法就是多一个进程或者一个线程。因为创建一个新的工作者进程的进程间通讯比创建一个新的线程更加的开销大,第二个方法是最普遍的选择。
这种API的最好的例子就是地址解析,它是将一个主机地址转换成IP的API。这个处理包括查询系统,域名解析系统,同时这个回应几乎是即时的,但远程服务器也可能失效,一些包可能丢失,网络连接可能断开,或者其它情况;简单地说,它在我们要等待一段时间才能得到回应。
在UNIX系统下唯一的标准API是堵塞的。
其它简单的例子还有独享读取和缩放。QImageReader和QImage只提供了堵塞方法从一个设备区读取图像,或者缩放到另一个分辨率。如果你正在处理相当大的图像,这些处理会占用很长的时间。
当你想去充分利用多核处理器
线程允许你的程序充分利用多核处理器。因为每一个线程由操作系统单独调度,如果你的程序运行在一个多核处理器上,你的多个线程可能在同一事件被不同的核心所运行。
举例,考虑一个从一系列图像中产生缩略图的应用程序。一个拥有n个线程的线程池,在系统中每一个cpu都可用,能够分散这些将图像改变分辨率产生缩略图的工作,有效的几乎线性提升了速度。
当你不想被其它堵塞
这是一个相当高深的话题,所以现在可以随意的粗略看看。一个漂亮的例子是在WebKit 中使用QNetworkAccessManager的用法中体现的。WebKit是一个先进的浏览器引擎,也就是说,一系列的类布局在上面来显示网页。Qt widget使用WebKit模块的是QWebView。
QNetworkAccessManager是一个管理HTTP请求和回应的Qt类,我们可以把它看做是一个浏览器的网络引擎。在Qt 4.8之前,它没有使用任何工作者线程,所有的网络处理是在和QNetworkAccessManager和它的QNetworkReplys处在的线程中处理的。
然而没有为网络使用线程是一个很好的注意,它也有一个主要的缺点:如果你不立即从套接字中读取数据,内核缓存将会被填满,数据包将会丢失,传输速度将会立刻急速下降。
套接字活动被Qt的事件循环处理。堵塞事件循环将会因此导致传输性能的损失,因为没人会注意到这儿有数据要读取。
但是什么东西会堵塞事件循环?坏消息是WebKit它自己!一旦一些数据被接受了,WebKit使用它们并把它们显示在网页视窗上面。不幸的是,这个显示过程是相当的复杂,开销很大,因此它会堵塞主事件循环一会,但是足够去影响接下来的传输了。
总结起来,发生了这些事情:
WebKit发起了一个请求;
回应处发来的数据开始传输过来;
Webkit开始利用已到来的数据显示在视窗上,堵塞了事件循环;
没有了运行着的事件循环,数据被OS接受,但是不被QNetworkAccessManager套接字读取。
内核缓冲区将被填满,传输速度减慢。
总的页面下载时间也因此被自己给拖缓了。
注意到因为QNetworkAccessManager和QNetworkReply是QObject对象,它们不是线程安全的,因此不能仅仅把它们移动到你想继续使用它们的线程,因为它们可能同时被两个线程访问:你的和它们存在的,因为事件会被后面的线程事件循环给分发。
在Qt4.8中,qNetworkAccessManager现在默认分另一个线程来处理HTTP请求,所以GUI的无响应和OS的缓冲区增长太快的问题得到了修复。