QT之深入理解QThread
理解QThread之前需要了解下QThread類,QThread擁有的資源如下(摘錄于QT 5.1 幫助文檔):
在以上資源中,本文重點關注槽:start();信號:started()、finished();受保護的方法:run()、exec();
理解QThread
QThread與通常所熟知的線程(thread)有很大出入,在面向過程的語言中,我們建立一個線程的同時會傳入一個函數名,這個函數名代表該線程要執行的具體代碼(如圖 1 所示)。
圖 1. 我們通常所理解的線程
但是QThread里并沒有線程的具體代碼,QThread只是一個接口而已,目的是為操作系統提供一個用于線程調度的“句柄”。這個“句柄”即是QThread的入口(如圖 2 所示)。
圖 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.?”具體參見:<http://blog.qt.digia.com/blog/2010/06/17/youre-doing-it-wrong/>。
? ??那么,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(); }; |
上面這段程序的問題在哪兒呢?正如原文所說:“We’re telling the thread to run code “in itself”.We’re also doing this?before?the thread is running as well. Even though this seems to work, it’s confusing, and not how QThread was designed to be used (all of the functions in QThread were written and intended to be called from the creating thread, not the thread that QThread starts).”
總結起來,問題有兩點: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()信號。
這只是一個示例,代碼的編寫更注重演示效果,而非其他。
該示例的工程組織如下:
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;
}
--------------------------------------------------------------------------
http://blog.csdn.net/desert187/article/details/37932999