Qt入门(9)——Qt中的线程支持
Qt对线程提供了支持,基本形式有独立于平台的线程类、线程安全方式的事件传递和一个全局Qt库互斥量允许你可以从不同的线程调用Qt方法。
警告:所有的GUI类(比如,QWidget和它的子类),操作系统核心类(比如,QProcess)和网络类都不是线程安全的。
QRegExp使用一个静态缓存并且也不是线程安全的,即使通过使用QMutex来保护的QRegExp对象。
启用线程支持
在Windows上安装Qt时,在一些编译器上线程支持是一个选项。
在Mac OS X和Unix上,线程支持可以当你在运行configure脚本时添加-thread选项就可以生效了。在Unix平台上,多线程程序必须用特殊的方式连接,比如使用特殊的libc,安装程序将会创建另外一个库libqt-mt并且因此线程程序必须和这个库进行连接(使用-lqt-mt)而不是标准的Qt库。
在两个平台上,你都应该定义宏QT_THREAD_SUPPORT来编译(比如,编译时使用-DQT_THREAD_SUPPORT)。在Windows上,这个通常可以在qconfig.h写一个条目来解决。
线程类
最重要的类是QThread,也就是说要开始一个新的线程,就是开始执行你重新实现的QThread::run()。这和Java的线程类很相似。
为了写线程程序,在两个线程同时希望访问同一个数据时,对数据进行保护是很必要的。因此这里也有一个QMutex类,一个线程可以锁定互斥量,并且在它锁定之后,其它线程就不能再锁定这个互斥量了,试图这样做的线程都会被阻塞直到互斥量被释放。例如:
class MyClass { public: void doStuff( int ); private: QMutex mutex; int a; int b; }; // 这里设置a为c,b为c*2。 void MyClass::doStuff( int c ) { mutex.lock(); a = c; b = c * 2; mutex.unlock(); }
这保证了同一时间只有一个线程可以进入MyClass::doStuff(),所以b将永远等于c * 2。
另外一个线程也需要在一个给定的条件下等待其它线程的唤醒,QWaitCondition类就被提供了。线程等待的条件QWaitCondition指出发生了什么事情,阻塞将一直持续到这种事情发生。当某种事情发生了,QWaitCondition可以唤醒等待这一事件的线程之一或全部。(这和POSIX线程条件变量是具有相同功能的并且它也是Unix上的一种实现。)例如:
#include <qapplication.h> #include <qpushbutton.h> // 全局条件变量 QWaitCondition mycond; // Worker类实现 class Worker : public QPushButton, public QThread { Q_OBJECT public: Worker(QWidget *parent = 0, const char *name = 0) : QPushButton(parent, name) { setText("Start Working"); // 连接从QPushButton继承来的信号和我们的slotClicked()方法 connect(this, SIGNAL(clicked()), SLOT(slotClicked())); // 调用从QThread继承来的start()方法……这将立即开始线程的执行 QThread::start(); } public slots: void slotClicked() { // 唤醒等待这个条件变量的一个线程 mycond.wakeOne(); } protected: void run() { // 这个方法将被新创建的线程调用…… while ( TRUE ) { // 锁定应用程序互斥锁,并且设置窗口标题来表明我们正在等待开始工作 qApp->lock(); setCaption( "Waiting" ); qApp->unlock(); // 等待直到我们被告知可以继续 mycond.wait(); // 如果我们到了这里,我们已经被另一个线程唤醒……让我们来设置标题来表明我们正在工作 qApp->lock(); setCaption( "Working!" ); qApp->unlock(); // 这可能会占用一些时间,几秒、几分钟或者几小时等等,因为这个一个和GUI线程分开的线程,在处理事件时,GUI线程不会停下来…… do_complicated_thing(); } } }; // 主线程——所有的GUI事件都由这个线程处理。 int main( int argc, char **argv ) { QApplication app( argc, argv ); // 创建一个worker……当我们这样做的时候,这个worker将在一个线程中运行 Worker firstworker( 0, "worker" ); app.setMainWidget( &worker ); worker.show(); return app.exec(); }
只要你按下按钮,这个程序就会唤醒worker线程,这个线程将会进行并且做一些工作并且然后会回来继续等待被告知做更多的工作。如果当按钮被按下时,worker线程正在工作,那么就什么也不会发生。当线程完成了工作并且再次调用QWaitCondition::wait(),然后它就会被开始。
线程安全的事件传递
在Qt中,一个线程总是一个事件线程——确实是这样的,线程从窗口系统中拉出事件并且把它们分发给窗口部件。静态方法QThread::postEvent从线程中传递事件,而不同于事件线程。事件线程被唤醒并且事件就像一个普通窗口系统事件那样在事件线程中被分发。例如,你可以强制一个窗口部件通过如下这样做的一个不同的线程来进行重绘:
QWidget *mywidget; QThread::postEvent( mywidget, new QPaintEvent( QRect(0, 0, 100, 100) ) );
这(异步地)将使mywidget重绘一块100*100的正方形区域。
Qt库互斥量
Qt库互斥量提供了从线程而不是事件线程中调用Qt方法的一种方法。例如:
QApplication *qApp; QWidget *mywidget; qApp->lock(); mywidget->setGeometry(0,0,100,100); QPainter p; p.begin(mywidget); p.drawLine(0,0,100,100); p.end(); qApp->unlock();
在Qt中没有使用互斥量而调用一个函数通常情况下结果将是不可预知的。从另外一个线程中调用Qt的一个GUI相关函数需要使用Qt库互斥量。在这种情况下,所有可能最终访问任何图形或者窗口系统资源的都是GUI相关的。使用容器类,字符串或者输入/输出类,如果对象只被一个线程使用就不需要任何互斥量了。
当进行线程编程时,需要注意的一些事情:
当使用Qt库互斥量的时候不要做任何阻塞操作。这将会冻结事件循环。
确认你锁定一个递归QMutex的次数和解锁的次数一样,不能多也不能少。
在调用除了Qt容器和工具类的任何东西之前锁定Qt应用程序互斥量。
谨防隐含地共享类,你应该避免在线程之间使用操作符=()来复制它们。这将会在Qt的未来主要的或次要的发行版本中进行改进。
谨防那些没有被设计为线程安全的Qt类,例如,QPtrList的应用程序接口就不是线程安全的并且如果不同的线程需要遍历一个QPtrList,它们应该在调用QPtrList::first()之前锁定并且在到达终点之后解锁,而不是在QPtrList::next()的前后进行锁定和解锁。
确认只在GUI线程中创建的继承和使用了QWidget、QTimer和QSocketNotifier的对象。在一些平台上,在某个不是GUI线程的线程中创建这样的对象将永远不会接受到底层窗口系统的事件。
和上面很相似,只在GUI线程中使用QNetwork类。一个经常被问到的问题是一个QSocket是否可以在多线程中使用。这不是必须得,因为所有的QNetwork类都是异步的。
不要在不是GUI线程的线程中试图调用processEvents()函数。这也包括QDialog::exec()、QPopupMenu::exec()、QApplication::processEvents()和其它一些。
在你的应用程序中,不要把普通的Qt库和支持线程的Qt库混合使用。这也就是说如果你的程序使用了支持线程的Qt库,你就不应该连接普通的Qt库、动态的载入普通Qt库或者动态地连接其它依赖普通Qt库的库或者插件。在一些系统上,这样做会导致Qt库中使用的静态数据变得不可靠了。