QThread學習

工作需要,前段時間要寫TCP連接的模塊,因爲連接本身可能是一個耗時的操作,而且要實現自動重連功能,所以必須要用到多線程,因此有機會學習QThread,現將體會成文。

首先是QThread::exec(),這個函數將進入當前線程的事件循環(網上很多文章都寫成時間循環),調用這個函數後將會阻塞線程,這時線程中的事件傳遞纔有效,信號與槽的連接纔有效,此時信號發射後槽函數纔會被調用。當執行了線程的quit()或exit()函數時exec()函數才返回。

看程序

//mythread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
class QTimer ;
class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = 0);
protected:
    void run();
public slots:
    void slotFunc();
private:
    QTimer *timer;    
};
#endif // MYTHREAD_H

//mythread.cpp
#include "mythread.h"
#include <QtDebug>
#include <QTimer>
MyThread::MyThread(QObject *parent) :
    QThread(parent)
{
}
void MyThread::run()
{
    timer = new QTimer;
    timer->setSingleShot(true);
    timer->start(5000);
	qDebug() << timer << QThread::currentThreadId();
    connect(timer, SIGNAL(timeout()), this, SLOT(slotFunc()));
    exec();
    qDebug() << "********************";
}
void MyThread::slotFunc()
{
    qDebug() << timer << QThread::currentThreadId();
    qDebug() << "#################";
}

//main.cpp
#include <QApplication>
#include "mythread.h"
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MyThread obj;
    obj.start();
    return a.exec();
}

程序運行輸出結果:

QTimer(0xb51019e8)3047508848

QTimer(0xb51019e8)3078874896

#################

由輸出結果可知發射信號和槽函數執行正常,這時可能就有人這樣思考,exec()是阻塞線程的,同樣死循環也是阻塞的,而且更容易被我們控制(我就是這樣想的),下面我們嘗試一下,將run()函數中的exec()替換爲

    int time = 5;
    while(time > 0) {
        sleep(2);
        time--;
    }

然後運行程序,輸出:

QTimer(0xb50019e8)3046460272

********************

由此可見,要真正實現子線程中的信號與槽的連接,必須得執行exec()函數進入子線程的事件循環。其實這個函數我們並不陌生,Qt的main函數中基本上都有如下的程序:

    QApplication a(argc, argv);
    ...    //do something
    return a.exec();

其實這段程序就是要進入主線程的事件循環,這樣事件纔會被傳遞處理,若寫成return 0;,程序會異常退出。

 

細心的讀者可能已經發現,在第一個程序的輸出結果中,timer的地址相同,但它卻處於兩個不同的線程中,也就是說,timer在一個線程中創建,然後在另一個線程中執行,如果在這個線程中執行刪除操作則可能會出現意想不到的結果,除非mutex用的好。這時如果將slotFunc()函數修改爲

void MyThread::slotFunc()
{
    qDebug() << timer << QThread::currentThreadId();
    qDebug() << "#################";
    timer->start(5000);
}

然後執行程序,

QTimer(0xb51019e8) 3047361392

QTimer(0xb51019e8)3078637328

#################

程序僅僅輸出一次,而不是像預期的那樣,每隔5s輸出一次,這就是因爲timer是處在兩個不同的線程中,而如果我們改變一下信號與槽的連接方式:

connect(timer, SIGNAL(timeout()), this, SLOT(slotFunc()), Qt::DirectConnection);

運行程序,有

QTimer(0xb50019e8)3046460272

QTimer(0xb50019e8)3046460272

#################

QTimer(0xb50019e8)3046460272

#################

QTimer(0xb50019e8)3046460272

#################

這樣程序就會每隔5s輸出一次,這時timer已經處於同一個線程了(可以通過改變信號與槽的連接方式改變槽函數的執行方式,不明白請了解Qt信號與槽的連接方式)。


上面講解了exec()函數,對於多線程程序中函數執行在哪個線程的問題,請參考博客http://blog.csdn.net/sydnash/article/details/7425947,要說明的是,如果在博客的mainwindow.cpp的構造函數中connect(firstButton,SIGNAL(clicked()),this,SLOT(onFirstPushed()));這樣創建連接,同樣是調用了MyObject::first()函數,但此時是槽函數時在主線程中執行的,另外,文章也說明了在使用線程的時候,可以將一個類派生自QObject,然後實現所有的signal/slot,然後通過調用movetothread函數來使他們執行在新的線程裏面,而不是每次都要重新派生QThread,並且派生QThread函數的另外一個不好的地方是隻有run函數內部的代碼纔會執行在新線程裏 面,相比起來,派生QObject並使用movetothread函數更具有靈活性,如同程序中執行QThread *thread = new Qthread; my->moveToThread(thread);。

 

另外,在Qt多線程編程中,如果用到QTcpSocket類,可能會出現如下警告:

QObject: Cannot create children for aparent that is in a different thread.

(Parent is QTcpSocket(0x8373b28), parent'sthread is QThread(0x81c9298),current thread is ConnThread(0x8373b18)

這是因爲創建和使用QTcpSocket對象不是在同一個線程,只要參照上面給出的博客的方法,使得在同一個線程創建和使用QTcpSocket對象就能避免警告。

 

最後,若有如下程序(.h文件未給出):

#include "newobject.h"
#include <QTcpSocket>
NewObject::NewObject(QObject *parent) :
    QObject(parent)
{
    readBuffer = new char[1500];
}
void NewObject::createVariables()
{
    socket = new QTcpSocket;
    connect(socket, SIGNAL(readyRead()), this, SLOT(readData()));
}
void NewObject::readData()
{
    while(socket->bytesAvailable()) {
        int size = socket->read(readBuffer, 1500);
        printf("size = %d\n", size);
    }
}

QThread *newThread = new QThread;
    newObj = new NewObject;
    newObj->moveToThread(newThread);
    newThread->start();

當執行下面代碼時,雖然不會報告任何警告或出錯信息,但是在執行newObj=newNewObject;時會執行readBuffer=newchar[1500];語句,這時程序執行在主線程中,也就是說,readBuffer是在主線程中創建的內存區域,而使用是在子線程中,這樣程序在執行過程中會出現意想不到的錯誤而導致崩潰(我本人爲此崩潰了許久,通過後面給出的方法修改終於穩定),因此要保證通過new創建對象和使用在同一個線程,可以將readBuffer=newchar[1500];置於createVariables()函數中,然後添加:

connect(newThread,SIGNAL(started()), newObj, SLOT(createVariables()));

同樣,通過下面語句來銷燬對象:

connect(newThread,SIGNAL(finished()), newObj, SLOT(releaseVariables()));

其中 releaseVariables()函數執行delete操作。


好了,就是這些,多線程編程本身就比較複雜,而Qt封裝了許多實現後若對其用法不是很明確會遇到各種各樣的異常,出現異常,很多時候是由於我們使用方法不對,而不是對多線程理解有誤,無論如何,我們只有在使用中摸索。

寫了這麼多,希望能夠幫到大家,不對之處歡迎大家指正~~~~~~



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章