轉自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