QT之深入理解QThread


 


    理解QThread之前需要了解下QThread类,QThread拥有的资源如下(摘录于QT 5.1 帮助文档):


 


    在以上资源中,本文重点关注槽:start();信号:started()、finished();受保护的方法:run()、exec();


 


理解QThread


    QThread与通常所熟知的线程(thread)有很大出入,在面向过程的语言中,我们建立一个线程的同时会传入一个函数名,这个函数名代表该线程要执行的具体代码(如图 1 所示)。


QT之深入理解QThread_#endif


图 1. 我们通常所理解的线程


    但是QThread里并没有线程的具体代码,QThread只是一个接口而已,目的是为操作系统提供一个用于线程调度的“句柄”。这个“句柄”即是QThread的入口(如图 2 所示)。


QT之深入理解QThread_事件处理_02


图 2. QThread是“面向对象的”


    QThread的入口多种多样,可以是槽函数,也可能是某个事件处理函数,但是由于是由系统调度的,因此这些函数的“准确”执行时刻是无法预知的。


    QThread的出口是finished()信号。


    作为线程,QThread会毫不犹豫的为自己创建一个运行空间,一个单独的执行线索,一个新的线程,但是翻阅QThread所拥有的资源,我们找不到传入函数名的地方,因此我们仿佛无法为这个新创建的线程提供具体的执行代码。


    很多人因此想到了run()方法,因而继承QThread函数,并将自己的代码写在run()方法中,往往要求run()方法不可以立刻退出,因此加入循环体和wait()方法,有时候为了响应事件而调用exec()进行堵塞。但这种做法是不建议的,已有文章指出“QThread was designed and is intended to be used as an interface or a control point to an operating system thread, not as a place to put code that you want to run in a thread. ” 


    那么,QThread真的不能执行具体代码么?如果不是,怎样将要在新线程中执行的程序交付给QThread呢?答案是moveToThread()方法。任何基于QObject类的子类都具有该方法。某个对象被moveTo到新线程后,它所具有的槽函数和事件处理程序都会被移动到新线程所在的运行空间中,成为新线程与操作系统之间的接口,即成为了新线程的入口。当有与这个槽连接的信号或与之相配的事件发生时,槽函数和事件处理程序将会在新线程空间中执行。


    如果只到此为止,那么很容易出现另一个问题,也就是上面连接中所举的例子。我们在这里详细说明。程序如下:



1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18




​class​​ ​​MyThread : ​​​​public​​ ​​QThread​


​{​


​public​​​​:​


​MyThread()​


​{​


​moveToThread(​​​​this​​​​);​


​}​


 


​void​​ ​​run();​


 


​signals:​


​void​​ ​​progress(​​​​int​​​​);​


​void​​ ​​dataReady(QByteArray);​


 


​public​​ ​​slots:​


​void​​ ​​doWork();​


​void​​ ​​timeoutHandler();​


​};​



  

    总结起来,问题有两点:1.在构造函数中moveToThread(),此时MyThread还没有开始运行;2.将MyThread移动到它自己空间去运行后,我们失去了对MyThread的引用。以上两点都容易导致非常致命的问题。可见,我们为了让代码在新线程中得以执行,我们实在有点儿太“不择手段”了。


    出现以上问题的根本原因在于,并没有充分理解QThread只是一个接口的本质。那么应该如何正确的让程序在新线程中得以执行呢?答案是将需要在新线程中运行的对象moveTo到QThread中,而非继承QThread并把自身moveTo到新线程空间中。


    由此我们提出应用QThread的以下几个重要原则。


 


QThread应用原则:


1.QThread只是系统执行线程的接口而已,并不是用于编写代码的;


2.在当前线程(如:线程A)上下文中创建的对象属于当前线程,其他线程(如:线程B、C、D...)不可以操作属于当前线程(如:线程A)的对象;


3.当前线程(如:线程A)中基于OBject类的对象可以被移动到其他线程(如:线程B、C、D...);


4.当前线程(如:线程A)中基于OBject类的对象在移动到其他线程(如:线程B、C、D...)去执行的时候,要求目标线程(如:线程B、C、D...)已经开始运行;


 


    由2可以推出,如果当前线程(如:线程A)中,基于OBject类的对象被移动到其他线程(如:线程B、C、D...)之后,该对象只能由目标线程(如:线程B、C、D...)负责释放。


    另外,在将信号与被moveTo到新线程中的对象所拥有的槽相连接时,需要注意连接的方式。


 


注意:


    信号与槽的连接方式有:Qt::AutoConnection、Qt::DirectConnection、Qt::QueuedConnection和Qt::BlockingQueuedConnection。


Qt::AutoConnection:是根据对象所在线程不同而选择Qt::DirectConnection或Qt::QueuedConnection;


Qt::DirectConnection:用于同一个线程当中,相当于直接函数调用,槽函数执行完后才返回;


Qt::QueuedConnection:用于不同的线程当中,会建立一个队列,槽函数立即返回,而不用等待队列中的信号执行完毕;


Qt::BlockingQueuedConnection:也是用于不同线程的,但是又相当于函数调用,因为要等到槽函数执行完毕才能够返回。


 


示例:


    在此,提供一个应用QThread的示例,该示例中打开一个串口用于接收数据,但为了同时兼顾UI对用户的响应,需要为串口接收程序单独建立一个线程。由于串口对象被moveTo到了新线程中,因此无法在UI线程中关闭串口,因此要用到QThread的finished()信号。


    这只是一个示例,代码的编写更注重演示效果,而非其他。


    该示例的工程组织如下:


    QT之深入理解QThread_事件处理_03


uiwindow.ui文件中窗体为初始化状态。


Serial.pro 文件内容如下:



--------------------------------------------------------------------------



#-------------------------------------------------
#
# Project created by QtCreator 2014-07-18T15:41:22
#
#-------------------------------------------------
QT       += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
greaterThan(QT_MAJOR_VERSION, 4) {
QT       += widgets serialport
} else {
include($$QTSERIALPORT_PROJECT_ROOT/src/serialport/qt4support/serialport.prf)
}
TARGET = Serial
TEMPLATE = app
SOURCES += main.cpp\
uiwindow.cpp \
serial.cpp
HEADERS  += uiwindow.h \
serial.h
FORMS    += uiwindow.ui
--------------------------------------------------------------------------
serial.h 文件内容如下:
--------------------------------------------------------------------------
#ifndef SERIAL_H
#define SERIAL_H
#include <QObject>
#include <QtSerialPort/QSerialPort>
class Serial : public QObject
{
Q_OBJECT
public:
explicit Serial(QObject *parent = 0);
~Serial(void);
QSerialPort *port;
signals:
public slots:
void readData(void);
void threadStarted(void);
void threadFinished(void);
};
#endif // SERIAL_H
--------------------------------------------------------------------------
serial.cpp 文件内容如下:
--------------------------------------------------------------------------
#include "serial.h"
#include <QMessageBox>
#include <QDebug>
#include <QThread>
Serial::Serial(QObject *parent) :
QObject(parent)
{
port = new QSerialPort();
port->setPortName("COM1");
if(!port->open(QSerialPort::ReadWrite))
{
QMessageBox WrrMsg;
WrrMsg.setInformativeText("无法打开该串口");
WrrMsg.show();
WrrMsg.exec();
}
port->setBaudRate(QSerialPort::Baud19200,QSerialPort::AllDirections);   // 19200,N,8,1
port->setDataBits(QSerialPort::Data8);
port->setStopBits(QSerialPort::OneStop);
port->setParity(QSerialPort::NoParity);
port->setFlowControl(QSerialPort::NoFlowControl);
connect(port, SIGNAL(readyRead()), this, SLOT(readData()), Qt::DirectConnection);   // 注意,真正执行时 port 与 Serial 在同一个线程中,因此使用 Qt::DirectConnection。
}
Serial::~Serial(void)
{
}
void Serial::readData(void)
{
qDebug()<< "Reading Data...ID is:" << QThread::currentThreadId();
port->clear(QSerialPort::AllDirections);
}
void Serial::threadStarted(void)
{
qDebug()<< "Thread has started...ID is:" << QThread::currentThreadId();
}
void Serial::threadFinished(void)
{
qDebug()<< "Closing COM port...ID is:" << QThread::currentThreadId();
if(port->isOpen())
{
port->close();      // 关闭串口。
}
}
--------------------------------------------------------------------------
uiwindow.h 文件内容如下:
--------------------------------------------------------------------------
#ifndef UIWINDOW_H
#define UIWINDOW_H
#include <QMainWindow>
#include <QThread>
#include "serial.h"
namespace Ui {
class UIWindow;
}
class UIWindow : public QMainWindow
{
Q_OBJECT
public:
explicit UIWindow(QWidget *parent = 0);
~UIWindow();
private:
Ui::UIWindow *ui;
QThread serialThread;
Serial *serial;
};
#endif // UIWINDOW_H
--------------------------------------------------------------------------
uiwindow.cpp 文件内容如下:
--------------------------------------------------------------------------
#include "uiwindow.h"
#include "ui_uiwindow.h"
#include <QDebug>
UIWindow::UIWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::UIWindow)
{
ui->setupUi(this);
qDebug()<< "UI thread ID is:" << QThread::currentThreadId();
serial = new Serial();
connect(&serialThread, SIGNAL(started()), serial, SLOT(threadStarted()), Qt::QueuedConnection);     // 注意,serialThread 与 serial 并不在同一个线程中,因此使用 Qt::QueuedConnection。
connect(&serialThread, SIGNAL(finished()), serial, SLOT(threadFinished()), Qt::DirectConnection);   // serialThread 的 finished() 信号是在新线程中执行的,因此此处要使用 Qt::DirectConnection。
serialThread.start(QThread::HighestPriority);   // 开启线程,串口接收线程的优先级较高。
serial->moveToThread(&serialThread);            // 将串口接受对象移动到新线程中。
serial->port->moveToThread(&serialThread);      // 用于接收的 port 一并移入新线程中。
}
UIWindow::~UIWindow()
{
if(serialThread.isRunning())
{
serialThread.exit();                // 结束该线程。
serialThread.wait();
/*while(!serialThread.isFinished())
{
;
}*/
}
delete ui;
}
--------------------------------------------------------------------------