轉載豆子 Qt 學習之路 2(71):線程簡介

前面我們討論了有關進程以及進程間通訊的相關問題,現在我們開始討論線程。事實上,現代的程序中,使用線程的概率應該大於進程。特別是在多核時代,隨着 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 交互的情況。稍懂多線程開發的童鞋都會知道,調試多線程開發簡直就是煎熬。下面幾章,我們會更詳細介紹有關多線程編程的相關內容。

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