前面我們討論了有關進程以及進程間通訊的相關問題,現在我們開始討論線程。事實上,現代的程序中,使用線程的概率應該大於進程。特別是在多核時代,隨着 CPU 主頻的提升,受制於發熱量的限制,CPU 散熱問題已經進入瓶頸,另闢蹊徑地提高程序運行效率就是使用線程,充分利用多核的優勢。有關線程和進程的區別已經超出了本章的範疇,我們簡單提一句,一個進程可以有一個或更多線程同時運行。線程可以看做是“輕量級進程”,進程完全由操作系統管理,線程即可以由操作系統管理,也可以由應用程序管理。
Qt 使用QThread
來管理線程。下面來看一個簡單的例子:
///!!! Qt5
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QWidget *widget = new QWidget(this);
QVBoxLayout *layout = new QVBoxLayout;
widget->setLayout(layout);
QLCDNumber *lcdNumber = new QLCDNumber(this);
layout->addWidget(lcdNumber);
QPushButton *button = new QPushButton(tr("Start"), this);
layout->addWidget(button);
setCentralWidget(widget);
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, [=]() {
static int sec = 0;
lcdNumber->display(QString::number(sec++));
});
WorkerThread *thread = new WorkerThread(this);
connect(button, &QPushButton::clicked, [=]() {
timer->start(1);
for (int i = 0; i < 2000000000; i++);
timer->stop();
});
}
我們的主界面有一個用於顯示時間的 LCD 數字面板還有一個用於啓動任務的按鈕。程序的目的是用戶點擊按鈕,開始一個非常耗時的運算(程序中我們以一個 2000000000 次的循環來替代這個非常耗時的工作,在真實的程序中,這可能是一個網絡訪問,可能是需要複製一個很大的文件或者其它任務),同時 LCD 開始顯示逝去的毫秒數。毫秒數通過一個計時器QTimer
進行更新。計算完成後,計時器停止。這是一個很簡單的應用,也看不出有任何問題。但是當我們開始運行程序時,問題就來了:點擊按鈕之後,程序界面直接停止響應,直到循環結束纔開始重新更新。
有經驗的開發者立即指出,這裏需要使用線程。這是因爲 Qt 中所有界面都是在 UI 線程中(也被稱爲主線程,就是執行了QApplication::exec()
的線程),在這個線程中執行耗時的操作(比如那個循環),就會阻塞 UI 線程,從而讓界面停止響應。界面停止響應,用戶體驗自然不好,不過更嚴重的是,有些窗口管理程序會檢測到你的程序已經失去響應,可能會建議用戶強制停止程序,這樣一來你的程序可能就此終止,任務再也無法完成。所以,爲了避免這一問題,我們要使用 QThread 開啓一個新的線程:
///!!! Qt5
class WorkerThread : public QThread
{
Q_OBJECT
public:
WorkerThread(QObject *parent = 0)
: QThread(parent)
{
}
protected:
void run()
{
for (int i = 0; i < 1000000000; i++);
emit done();
}
signals:
void done();
};
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QWidget *widget = new QWidget(this);
QVBoxLayout *layout = new QVBoxLayout;
widget->setLayout(layout);
lcdNumber = new QLCDNumber(this);
layout->addWidget(lcdNumber);
QPushButton *button = new QPushButton(tr("Start"), this);
layout->addWidget(button);
setCentralWidget(widget);
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, [=]() {
static int sec = 0;
lcdNumber->display(QString::number(sec++));
});
WorkerThread *thread = new WorkerThread(this);
connect(thread, &WorkerThread::done, timer, &QTimer::stop);
connect(thread, &WorkerThread::finished, thread, &WorkerThread::deleteLater);
connect(button, &QPushButton::clicked, [=]() {
timer->start(1);
thread->start();
});
}
注意,我們增加了一個WorkerThread
類。WorkerThread
繼承自QThread
類,重寫了其run()
函數。我們可以認爲,run()
函數就是新的線程需要執行的代碼。在這裏就是要執行這個循環,然後發出計算完成的信號。而在按鈕點擊的槽函數中,使用QThread::start()
函數啓動一個線程(注意,這裏不是run()
函數)。再次運行程序,你會發現現在界面已經不會被阻塞了。另外,我們將WorkerThread::deleteLater()
函數與WorkerThread::finished()
信號連接起來,當線程完成時,系統可以幫我們清除線程實例。這裏的finished()
信號是系統發出的,與我們自定義的done()
信號無關。
這是 Qt 線程的最基本的使用方式之一(確切的說,這種使用已經不大推薦使用,不過因爲看起來很清晰,而且簡單使用起來也沒有什麼問題,所以還是有必要介紹)。代碼看起來很簡單,不過,如果你認爲 Qt 的多線程編程也很簡單,那就大錯特錯了。Qt 多線程的優勢設計使得它使用起來變得容易,但是坑很多,稍不留神就會被絆住,尤其是涉及到與 QObject 交互的情況。稍懂多線程開發的童鞋都會知道,調試多線程開發簡直就是煎熬。下面幾章,我們會更詳細介紹有關多線程編程的相關內容。