QThread與QWidget使用

轉自http://hi.baidu.com/cyclone/blog/item/65f3f603294f2e783812bb51.html

 

感謝作者 dbzhang800

 

 

本文主要內容:
在任務一中,用 四 種方式實現:點擊界面按鈕,開線程運行一段程序,結果顯示在一個Label上。
1. 用不正確的方式得到看似正確的結果
2. 用Qt Manual 和 例子中使用的方法
3. 用一種好用但被Qt開發人員批判的方法
4. 用一種被開發人員強烈推薦,但Qt Manual和例子中隻字未提的方法

  • 爲了簡單起見,本文只講如何做及其結果是什麼,而不講其原因是什麼(估計大家對原因也不會感興趣,詳見: QThread 使用探討  和 QThread使用方法)。

  • 本文只考慮兩個線程(即主線程和一個次線程)的情況。

QWidget

  • QWidget及其派生類均 不能在次線程中使用或創建

Manual 中的原話:

  • The GUI classes, notably QWidget and all its subclasses, are not reentrant. They can only be used from the main thread.
  • 因爲不允許,所以嘗試這麼做的,幾乎很快都能回頭。畢竟signals和slots用起來確實蠻方便
  • 但是,回頭後,就理解和用對 QThread 了麼?

QThread

概念一:QThread 對象本身所在的線程 和它管理的線程不是同一個線程。

  • 前者是主線程
  • 後者是次線程

概念二:你在QThread派生類中定義的槽在主線程而不是在次線程中執行的。

  • run 函數是線程的入口點,run內的代碼纔是在次線程中運行的代碼

概念三:除了Manual和Qt例子中給出的用法外,QThread有一種更容易且被推薦的使用方法:

  • QThread 應該被看做是操作系統線程的接口或控制點,而不應該包含需要在新線程中運行的代碼
  • 需要運行的代碼應該放到一個QObject的子類中,然後將該子類的對象moveToThread到新線程中。

關於本文的例子

  • 爲了代碼簡單,所有例子都是單一的源文件,保存爲 main.cpp
    • 你從代碼中包含的 #include“main.moc”應該能看出

  • 爲了省幾行代碼,頭文件都是直接包含 QtCore 和 QtGui。

  • 爲了清楚告訴大家槽函數分別是在那個線程運行的,調用了幾處 currentThreadId 函數
    • main 函數中輸出主線程IDqDebug()<<"main: "<<QThread::currentThreadId();

    • run 函數中輸出次線程IDqDebug()<<"thread: "<<currentThreadId();

    • 槽函數中輸出其在哪個線程中執行qDebug()<<"slots1: "<<currentThreadId();

  • 因爲用了qDebug,所以你的pro文件內最好加上 CONFIG+=console

  • 同樣爲了省代碼,例子中未考慮線程如何正常結束的問題。

任務一

點擊界面按鈕,開線程運行一段程序,結果顯示在一個Label上。

  • 定義一個Widget,上面放置 QPushButton 和 QLabel
  • 定義一個Thread,執行我們的代碼,然後通知 Widget

第一次嘗試

很容易想到方法,代碼可以工作,結果正確。但 ... 未必和你想得一樣

  • Thread 中定義一個slot1函數,接受數據,計算其立方,然後將結果通過信號發出
  • Widget 中按鈕每點擊一次,發出的數據加1,用label接受Thread的信號

 

#include <QtCore> 
#include <QtGui>

class Thread:public QThread
{
Q_OBJECT
public:
Thread(){}
public slots:
void slot1(int v)
{
qDebug()<<"slots1: "<<currentThreadId();
emit sig1(QString::number(v*v*v));
}
signals:
void sig1(const QString& t);
protected:
void run()
{
qDebug()<<"thread: "<<currentThreadId();
exec();
}
};

class Widget:public QWidget
{
Q_OBJECT
public:
Widget():m_label(new QLabel), m_button(new QPushButton("Button")), m_thread(new Thread)
{
QVBoxLayout * vbox = new QVBoxLayout(this);
vbox->addWidget(m_label);
vbox->addWidget(m_button);
setLayout(vbox);
connect(m_button,SIGNAL(clicked()),this,SLOT(onButtonClicked()));
connect(this,SIGNAL(clicked(int)),m_thread,SLOT(slot1(int)));
connect(m_thread,SIGNAL(sig1(QString)),m_label,SLOT(setText(QString)));
m_thread->start();
}
signals:
void clicked(int v);
private slots:
void onButtonClicked()
{
static int v = 0;
emit clicked(v);
v++;
}
private:
QLabel * m_label;
QPushButton * m_button;
Thread * m_thread;
};

#include "main.moc"
int main(int argc, char** argv)
{
QApplication app(argc, argv);
qDebug()<<"main: "<<QThread::currentThreadId();
Widget w;
w.show();
return app.exec();
}

一切工作正常,但看看控制檯輸出呢?

main:  3055777552 
thread: 3024481136
slots1: 3055777552
slots1: 3055777552
slots1: 3055777552
...

這兒明確告訴你,slot1 是在主線程中執行的。

嘗試二

我們試試 Qt Manual和 Qt 例子中採用的解決方案。

槽函數不是在主線程運行麼,而run函數不是次線程麼?那麼我們就:

  • 在槽函數中做個標記
  • 在run函數中根據標記進行運行

這樣以來,儘管槽函數在仍在主線程,但費時的計算代碼都在次線程了。

對Thread的類的改造如下(程序其他部分和 嘗試一 完全一樣):

 

class Thread:public QThread 
{
Q_OBJECT public:
Thread(){}
public slots:
void slot1(int v)
{
qDebug()<<"slots1: "<<currentThreadId();
m_mutex.lock();
m_vals.enqueue(v);
m_mutex.unlock();
}
signals:
void sig1(const QString& t);
protected:
void run()
{
qDebug()<<"thread: "<<currentThreadId();
while(1) {
m_mutex.lock();
if (!m_vals.isEmpty()){
int v = m_vals.dequeue();
emit sig1(QString::number(v*v*v));
}
m_mutex.unlock();
}
}
private:
QQueue<int> m_vals;
QMutex m_mutex;
};

注意哦,因爲 slot 函數在主線程中,而run函數在次線程中,所以二者需要 QMutex 實現對變量的安全訪問。如果你認真看過Qt自帶的例子,會發現它始終強調 QMutex 的使用。

嘗試三

嘗試二是"正統"的做法,但如過你用Google搜索過。那麼你可能不會選擇嘗試二,而是會使用下面的方法(其他部分和 嘗試一 完全一樣)

class Thread:public QThread 
{
Q_OBJECT public:
Thread(){ moveToThread(this); }
...

這樣以來,slot函數確實是在次線程工作的,看看控制檯輸出

main:  3056785168  
thread: 3024444272
slots1: 3024444272
slots1: 3024444272
...

很有意思?不是麼,一條 moveToThread(this),移動到自己。然後問題解決了。

  • 因爲前面說了,QThread 所在線程 和 它管理的線程不是同一個。
  • 這樣,其實將自己移動到自己所管理的線程了。

o(∩∩)o...哈哈,不要太高興哦,這個方法看起來比較舒服,但是它是被官方人員強烈批判的用法

嘗試四

終於到我想寫的代碼了,這是Qt線程的開發者建議的使用方式,但很可惜。直到目前(Qt4.7.0),手冊和例子中對此都隻字爲提。

  • 我們不子類話QThread了,我們只需要子類話一個QObject,然後將其move到QThread就行了,看代碼:
  • 不用子類化QThrad了,我們只需要子類話一個 QObject,需要在次線程中工作的代碼,直接放到它的槽中

 

class Worker:public QObject 
{
Q_OBJECT
public:
Worker(){}
public slots:
void slot1(int v)
{
qDebug()<<"slots1: "<<QThread::currentThreadId();
emit sig1(QString::number(v*v*v));
}
signals:
void sig1(const QString& t);
};
  • 因爲沒有Thread類了,只有Worker類,Widget代碼需做點改動

 

class Widget:public QWidget 
{
Q_OBJECT public:
Widget():m_label(new QLabel), m_button(new QPushButton("Button")), m_worker(new Worker)
{
QVBoxLayout * vbox = new QVBoxLayout(this);
vbox->addWidget(m_label);
vbox->addWidget(m_button);
setLayout(vbox);
QThread * thread = new QThread(this);
m_worker->moveToThread(thread);
connect(m_button,SIGNAL(clicked()),this,SLOT(onButtonClicked()));
connect(this,SIGNAL(clicked(int)),m_worker,SLOT(slot1(int)));
connect(m_worker,SIGNAL(sig1(QString)),m_label,SLOT(setText(QString)));
thread->start();
}
signals:
void clicked(int v);
private slots:
void onButtonClicked()
{
static int v = 0;
emit clicked(v);
v++;
}
private:
QLabel * m_label;
QPushButton * m_button;
Worker * m_worker;
};

main 函數還是和嘗試一完全一樣

控制檯輸出結果如下

main:  3056961296  
slots1: 3024616304
slots1: 3024616304
....

一共兩個線程,且二者id不同,說明slot在次線程中

恩。這篇文字似乎又不短了,看來任務二要另起一篇了。

  • -- dbzhang800 於 20101023

 

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