提要

Qt对线程提供了支持,基本形式有独立于平台的线程类、线程安全方式的事件传递和一个全局Qt库互斥量允许你可以从不同的线程调用Qt方法。

每个程序启动后就会拥有一个线程。该线程称为”主线程”(在Qt应用程序中也叫”GUI线程”)。Qt GUI必须运行在此线程上。所有的图形元件和几个相关的类,如QPixmap,不能工作于非主线程中。非主线程通常称为”工作者线程”,因为它主要处理从主线程中卸下的一些工作。

有时候,你需要的不仅仅是在另一线程的上下文中运行一个函数。您可能需要有一个生存在另一个线程中的对象来为 GUI线程提供服务。也许你想在另一个始终运行的线程中来轮询硬件端口并在有关注的事情发生时发送信号到GUI线程。Qt为开发多线程应用程序提供了多种 不同的解决方案。解决方案的选择依赖于新线程的目的以及线程的生命周期。


环境

Ubuntu 12.04 64bit

Qt 4.8.1


一个简单的例子

首先来看一个单线程的例子。

用Qt Creator创建一个Qt Gui工程,只有一个mainwindow类,代码如下:

mainwindow.h

#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QtGui/QMainWindow> #include <QPushButton> #include <QLabel> #include <QHBoxLayout> class MainWindow : public QMainWindow { Q_OBJECT private: QPushButton *calButton; QPushButton *hiButton; QLabel *mLabel; public: MainWindow(QWidget *parent = 0); ~MainWindow(); private slots: void slotGetPi(); void slotSayHi(); }; #endif // MAINWINDOW_H
mainwindow.cpp

#include "mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QHBoxLayout *mainLayout=new QHBoxLayout(); calButton = new QPushButton(this); calButton->setText("GetPi"); hiButton = new QPushButton(this); hiButton->setText("Hi"); mLabel = new QLabel(); mLabel->setText("Bitch"); mainLayout->setSpacing(10); mainLayout->addWidget(calButton); mainLayout->addWidget(hiButton); mainLayout->addWidget(mLabel); QWidget *centreWidget=new QWidget(this); centreWidget->setLayout(mainLayout); this->setCentralWidget(centreWidget); this->connect(calButton,SIGNAL(released()),this, SLOT(slotGetPi())); this->connect(hiButton,SIGNAL(released()),this, SLOT(slotSayHi())); } MainWindow::~MainWindow() { } void MainWindow::slotGetPi() { int time = 1000000000; float result=0; for(int i=1;i<=time;i++) { double value=4.0/(2*i-1); if (i % 2 == 1) result+=value; else result-=value; } mLabel->setText(QString::number(result)); } void MainWindow::slotSayHi() { mLabel->setText("Hei,gay~"); }
main.cpp

#include <QtGui/QApplication> #include "mainwindow.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }
代码很简单,就是一个窗口中有两个Button和一个label, 一个Button计算Pi,一个Button "say hi" 两个button都可以更新label.

预记的运行效果是点击button之后就可以改变label的值,但实际情况是...



因为我在点击GetPi这个Button的时候,程序就开始计算,当然是在主线程中,这时候整个界面就阻塞了,Hi Button 设置关闭窗口操作都无法完成,这时就不得不用线程了。


用线程改写一下。

创建一个ComputeThread类,继承自QThread。

computethread.h

#ifndef COMPUTETHREAD_H #define COMPUTETHREAD_H #include <QThread> #include <QDebug> #include <computethread.h> class ComputeThread : public QThread { Q_OBJECT private: void run(); public: explicit ComputeThread(QObject *parent = 0); signals: void computeFinish(double result); public slots: }; #endif // COMPUTETHREAD_H
computethread.cpp

#include "computethread.h" ComputeThread::ComputeThread(QObject *parent) : QThread(parent) { } void ComputeThread::run() { qDebug()<<this->currentThreadId()<<":Begin computing!"<<endl; int time = 1000000000; float result=0; for(int i=1;i<=time;i++) { double value=4.0/(2*i-1); if (i % 2 == 1) result+=value; else result-=value; } emit this->computeFinish(result); }
在run中定义线程运行的内容,还定义了一个信号,在计算完毕的时候将结果发射出去。


mainwindow中添加一个ComputeThread对象和一个槽。

private: ComputeThread *computePiThread; private slots: void slotShowResult(double result);
cpp中加入线程操作的部分:

#include "mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QVBoxLayout *mainLayout=new QVBoxLayout(); calButton = new QPushButton(this); calButton->setText("GetPi"); hiButton = new QPushButton(this); hiButton->setText("Hi"); mLabel = new QLabel(); mLabel->setText("Bitch"); computePiThread = new ComputeThread; mainLayout->setSpacing(10); mainLayout->addWidget(calButton); mainLayout->addWidget(hiButton); mainLayout->addWidget(mLabel); QWidget *centreWidget=new QWidget(this); centreWidget->setLayout(mainLayout); this->setCentralWidget(centreWidget); this->connect(computePiThread,SIGNAL(computeFinish(double)),this, SLOT(slotShowResult(double))); this->connect(hiButton,SIGNAL(released()),this, SLOT(slotSayHi())); this->connect(calButton,SIGNAL(released()),this, SLOT(slotGetPi())); } MainWindow::~MainWindow() { computePiThread->terminate(); computePiThread->wait(); delete computePiThread; computePiThread = 0; } void MainWindow::slotGetPi() { computePiThread->start(); } void MainWindow::slotSayHi() { mLabel->setText("Hei,gay~"); } void MainWindow::slotShowResult(double result) { mLabel->setText(QString::number(result)); }
运行结果



修改之后计算就在子线程中进行,主线程就没有卡死的情况了。


MultiThread in Opengl

之前有用QT作为框架来学习OpenGL,参考这里。

当是有个问题没有解决,就是当想要GLWidget中的图形不断的进行变换的话,就要在主线程中加一个死循环,这样做只是权宜之记,最好的解决方法就是用多线程。

创建一个GLThread类专门用来渲染:

glthread.h

#ifndef GLTHREAD_H #define GLTHREAD_H #include <QThread> #include <QSize> #include <QTime> #include<GL/glu.h> class GLWidget; class GLThread : public QThread { public: GLThread(GLWidget *glWidget); void resizeViewport(const QSize &size); void run(); void stop(); private: bool doRendering; bool doResize; int w; int h; GLWidget *glw; }; #endif // GLTHREAD_H

glthread.cpp

#include "glthread.h" #include "glwidget.h" GLThread::GLThread(GLWidget *gl) : QThread(), glw(gl) { doRendering = true; doResize = false; } void GLThread::stop() { doRendering = false; } void GLThread::resizeViewport(const QSize &size) { w = size.width(); h = size.height(); doResize = true; } void GLThread::run() { glw->makeCurrent(); this->rotAngle = 0.0; glClearColor(0.0f, 0.0f, 0.0f, 0.0f);// This Will Clear The Background Color To Black glClearDepth(1.0);// Enables Clearing Of The Depth Buffer glDepthFunc(GL_LESS);// The Type Of Depth Test To Do glEnable(GL_DEPTH_TEST);// Enables Depth Testing glShadeModel(GL_SMOOTH);// Enables Smooth Color Shading glMatrixMode(GL_PROJECTION); glLoadIdentity();// Reset The Projection Matrix gluPerspective(45.0f,(GLfloat)w/(GLfloat)h,0.1f,100.0f);// Calculate The Aspect Ratio Of The Window glMatrixMode(GL_MODELVIEW); while (doRendering) { rotAngle +=5; if(rotAngle>=360) rotAngle = 0; if (doResize) { glViewport(0, 0, w, h); doResize = false; glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0f,(GLfloat)w/(GLfloat)h,0.1f,100.0f); glMatrixMode(GL_MODELVIEW); } glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Clear The Screen And The Depth Buffer glLoadIdentity();// Reset The View glTranslatef(-1.5f,0.0f,-6.0f);// Move Left 1.5 Units And Into The Screen 6.0 glRotatef(rotAngle,0.0f,0.0f,1.0f);// Rotate The Triangle On The Y axis // draw a triangle (in smooth coloring mode) glBegin(GL_POLYGON);// start drawing a polygon glColor3f(1.0f,0.0f,0.0f);// Set The Color To Red glVertex3f( 0.0f, 1.0f, 0.0f);// Top glColor3f(0.0f,1.0f,0.0f);// Set The Color To Green glVertex3f( 1.0f,-1.0f, 0.0f);// Bottom Right glColor3f(0.0f,0.0f,1.0f);// Set The Color To Blue glVertex3f(-1.0f,-1.0f, 0.0f);// Bottom Left glEnd();// we're done with the polygon (smooth color interpolation) glw->swapBuffers(); msleep(50); qDebug("rendering"); } }

GLWidget也要进行相应的修改:

glwidget.h

#ifndef GLWIDGET_H #define GLWIDGET_H #include <QGLWidget> #include "glthread.h" #include <QResizeEvent> class GLWidget : public QGLWidget { public: GLWidget(QWidget *parent); void startRendering(); void stopRendering(); protected: void resizeEvent(QResizeEvent *evt); void paintEvent(QPaintEvent *); void closeEvent(QCloseEvent *evt); GLThread glt; }; #endif // GLWIDGET_H
glwidget.cpp

#include "glwidget.h" GLWidget::GLWidget(QWidget *parent) : glt(this) { setAutoBufferSwap(false); resize(320, 240); } void GLWidget::startRendering() { glt.start(); } void GLWidget::stopRendering() { glt.stop(); glt.wait(); } void GLWidget::resizeEvent(QResizeEvent *evt) { glt.resizeViewport(evt->size()); } void GLWidget::paintEvent(QPaintEvent *) { // Handled by the GLThread. } void GLWidget::closeEvent(QCloseEvent *evt) { stopRendering(); QGLWidget::closeEvent(evt); }
注意这里用到了C++的一个小技巧,前向声明。当两个类要互相引用的时候不能够互相包含头文件,在一个类的头文件中,必须用Class + 类名作为前向声明。而在这个类的cpp中要访问另一个类的具体方法的话,必须包含那个类的头文件。

这里还涉及到数据的访问。最开始的例子用的是信号槽的方式进行访问,而这里直接使用的指针进行访问。


渲染结果:一个不断旋转的正方形,(假装看见了...)



线程安全 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
线程安全问题都是由全局变量及静态变量引起的。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

一些琐碎

QtConcurrent

提供了一些高级API,使得写多线程程序可以不再使用像互斥、读写锁、等待条件、信号量等低级的多线程命令。用QtConcurrent写的程序可以根据内核数量自动调整线程数。这意味着今天写的应用程序将来可以部署在多核系统上。

尽管如此,QtConcurrent 不能用于线程运行时需要通信的情况,而且它也不应该被用来处理阻塞操作。

QReadWriteLock

是一个读写锁,主要用来同步保护需要读写的资源。当你想多个读线程可以同时读取资源,但是只能有一个写线程操作资源,而其他线程必须等待写线程完成时,这时候用这个读写锁就很有用了。可以实现多个读,一个写,读之间可以不同步不互斥,写时会阻塞其他的写操作。QReadWriteLock也有递归和非递归模式之分。

用法

QReadWriteLock lock; void ReaderThread::run() { lock.lockForRead(); read_file(); lock.unlock(); } void WriterThread::run() { lock.lockForWrite(); write_file(); lock.unlock(); }


参考

解析Qt中QThread使用方法 -http://mobile.51cto.com/symbian-268690_all.htm


Glimpsing the Third Dimension -http://doc.qt.digia.com/qq/qq06-glimpsing.html#writingmultithreadedglapplications