執行緒等待(QWaitCondition)

在執行緒的同步化時,通常執行緒執行各自的工作,但有些條件下執行緒必須等待,等待條件允許後才繼續進行工作,這時使用QWaitCondition來達到目的。


例如在生產者(Producer)與消費者(Consumer)的例子中,生產者會將產品交給店員,而消費者從店員處取走產品,店員一次只能持有固定數量產品,如果達到此數量,店員會叫生產者等一下,等到有空位再喚醒生產者繼續生產,如果店中沒有產品了,店員會叫消費者等一下,等到店中有產品了再喚醒消費者來取走產品。

以下範例生產者每次生產一個整數交給店員,而消費者從店員處取走整數,最後我們可以看到流程,依序生產->拿走->生產->拿走……。


clerk.h

#ifndef CLERK
#define CLERK

#include<QDebug>
#include <QMutex>
#include <QWaitCondition>

class Clerk {
public:
    Clerk();
    void setProduct(int product);
    void getProduct();
private:
    int m_product;
    QMutex mutex;
    QWaitCondition waitCondition;
};

#endif 

clerk.cpp

#include "clerk.h"

Clerk::Clerk() {
    m_product = -1;
}

void Clerk::setProduct(int product) {
    mutex.lock();

    if(m_product != -1) {
        waitCondition.wait(&mutex); //如果店員手上有產品,生產者執行緒停在此處
    }

    //開始生產產品
    m_product = product;      //生產後將店員手中產品編號
    qDebug() << "生產者產生: " << m_product ;
    waitCondition.wakeOne();  //喚醒消費者執行緒,可以開始拿走產品

    mutex.unlock();
}

void Clerk::getProduct() {
    mutex.lock();

    if(m_product == -1) {
        waitCondition.wait(&mutex);  //如果店員手上沒有產品,消費者執行緒停在此處
    }

    //開始拿走產品
    qDebug() << "消費者取走: " << m_product ;
    m_product = -1;           //取走後將店員手中產品編號設為-1
    waitCondition.wakeOne();  //喚醒生產者執行緒,可以開始生產產品

    mutex.unlock();
}

consumer.h

#ifndef CONSUMER
#define CONSUMER

#include <QThread>
#include"clerk.h"

class Consumer : public QThread {
public:
    Consumer(Clerk *clerk);
protected:
    void run();
private:
    Clerk *clerk;
};

#endif 

consumer.cpp

#include "consumer.h"
#include "clerk.h"

Consumer::Consumer(Clerk *clerk) {
    this->clerk = clerk;
}

void Consumer::run() {
    for(int i = 1; i <= 10; i++) {
        QThread::msleep(500);
        clerk->getProduct();
    }
}

producer.h

#include <QThread>
#include"clerk.h"

class Producer : public QThread {
public:
    Producer(Clerk *clerk);
protected:
    void run();
private:
    Clerk *clerk;
};

#endif 

producer.cpp

#include "producer.h"
#include "clerk.h"

Producer::Producer(Clerk *clerk) {
    this->clerk = clerk;
}

void Producer::run() {
    for(int product = 1; product <= 10; product++) {
        QThread::msleep(1000);
        clerk->setProduct(product);
    }
}

main.cpp

#include <QCoreApplication>
#include "clerk.h"
#include "producer.h"
#include "consumer.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Clerk *clerk = new Clerk;
    Producer *producer = new Producer(clerk);
    Consumer *consumer = new Consumer(clerk);

    producer->start();
    consumer->start();
    producer->wait();
    consumer->wait();

    return 0;
}

QWaitCondition

執行緒暫停與終止

如果想要停止執行緒,QThread有個terminate()方法,但是這個方法會直接停止執行緒,無論現在是在流程的哪個位置,這會使得一些資源的善後工作無法完成,可能導致不可預期的程式錯誤,因此不建議使用。

一個執行緒要停止,基本上就是執行完run()函式,此時系統會幫我們清除線程實例。但有時可能run()函式執行的是一個無窮迴圈,由使用者提供訊息,程式接收到訊息後才終止執行緒。


這邊的範例有兩個按鈕,一個開啟執行緒,一個關閉執行緒,程式內部有個stopped變數,在run()方法中,while迴圈由stopped變數來判斷是否繼續迴圈。

thread.h

#ifndef THREAD
#define THREAD

#include <QThread>
#include <QDebug>

class Thread : public QThread {
    Q_OBJECT

public:
    Thread();
    void stop();
protected:
    void run();
private:
    bool stopped;
};

#endif

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QPushButton>
#include <QHBoxLayout>
#include <QCloseEvent>
#include "thread.h"

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
public slots:
    void startThread();
    void stopThread();
protected:
    void closeEvent(QCloseEvent *event);
private:
    QPushButton *startBtn;
    QPushButton *stopBtn;
    QHBoxLayout *layout;
    Thread *thread;
};

#endif 

widget.cpp

#include "widget.h"

Widget::Widget(QWidget *parent) : 
    QWidget(parent)
{
    setWindowTitle(tr("按鍵組"));
    layout = new QHBoxLayout(this);
    startBtn = new QPushButton(tr("開啟執行緒"));
    stopBtn = new QPushButton(tr("關閉執行緒"));
    layout->addWidget(startBtn);
    layout->addWidget(stopBtn);

    startBtn->setEnabled(true);
    stopBtn->setEnabled(false);

    thread = new Thread();
    connect(startBtn,SIGNAL(clicked()),this,SLOT(startThread()));
    connect(stopBtn,SIGNAL(clicked()),this,SLOT(stopThread()));
}

void Widget::startThread() {
    thread->start();
    startBtn->setEnabled(false);
    stopBtn->setEnabled(true);
}

void Widget::stopThread() {
    if(thread->isRunning()){
        thread->stop();
    }
    stopBtn->setEnabled(false);
    startBtn->setEnabled(true);
}
void Widget::closeEvent(QCloseEvent *event) {
    thread->stop();
    thread->wait();
    event->accept();
}

main.cpp

#include "widget.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();

    return a.exec();
}

  • QThread的wait()方法,會等待執行緒完成,再進行接下來的動作,也可以指定wait()的時間,時間到時就進行接下來的動作。

執行緒同步(QMutex)

如果一個物件所持有的資料,可以被多個執行緒同時寫入和讀取時,必須考慮到資料同步的問題,像假如我們有以下的類別:

class Counter{
private:
    int n ;
public:
    counter() { n=0;}
    void increment() {n++;}
    void decrement() {n--;}
    int getValue() {return n;}
};

這個類別不是線程安全的,如果讓多個線程修改成員變數n,結果是不可預測的,因為++和–不是原子操作,它們會被分成3到機器指令,第一條指令向暫存器存入變數的值,第二條指令遞增或遞減暫存器的值,第三條指令將暫存器的值存回變數。如果有兩個線程同時加載變數的值,更改後再存回,結果可能相互覆蓋,就像是如果變數n原本為0,有兩個執行緒同時進行遞增操作,可能因為覆蓋使得結果為1,而不是預期的2。

為了避免這種情況,我們可以加上QMutex,當執行緒執行到QMutex的lock()時,此時會鎖定接下來的程式流程,其它嘗試執行此流程的執行緒必須等待,等到此執行緒執行到QMutex的unlock()後,才可以進行,我們對之前的類別進行改寫,使得此類別的遞增和遞減在多執行緒操作下是安全的。

class Counter {
private:
    QMutex mutex;
    int n ;
public:
    counter() { n=0;}
    void increment() {mutex.lock(); n++; mutex.unlock();}
    void decrement() {mutex.lock(); n--; mutex.unlock();}
    int getValue() {return n;}
};

也可以使用QMutexLocker,建構時以QMutex物件作為引數並進行鎖定,QMutexLocker解構時自動解除鎖定,兩者效果相同,以下為改寫的遞增遞減函式。

class Counter {
private:
    QMutex mutex;
    int n ;
public:
    counter() { n=0;}
    void increment() {QMutexLocker locker(&mutex); n++; }
    void decrement() { QMutexLocker locker(&mutex); n--;}
    int getValue() {return n;}
};

執行緒介紹(QThread)

一個執行緒(Thread)是程序(Process)中的一個執行流程,一個程序可以同時包括多個執行緒,使得一個程式可以像是同時處理多個事務,例如可以一方面接受網路上的資料,另一方面同時接受使用者輸入的訊息。

Qt有些類別,本身操作就設計為非同步,像Signal與Slot的使用,不用另外增加多執行緒,也可以實現非阻斷的操作,但有些時候,我們必須實作多執行緒,才不會組塞主執行緒流程。


以下範例有一個用來顯示時間的面板,還有一個用於開啟耗時計算的按鈕,當使用者點擊此按鈕,程式開始一個冗長的for迴圈,實務上可能是其他需要耗時的功能。一開始程式正常更新時間,但是當我們點擊按鈕之後,面板停止更新時間,且程序界面組塞,直到計算結束才恢復正常。


widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QTimer>
#include <QTime>
#include <QLCDNumber>
#include <QVBoxLayout>
#include <QPushButton>
#include <QMessageBox>

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
private:
    QTimer *myTimer;
    QLCDNumber *myLCDNumber;
    QVBoxLayout *layout;
    QPushButton *startBtn;
private slots:
    void showTime();
    void startCalc();
};

#endif 

widget.cpp

#include "widget.h"

Widget::Widget(QWidget *parent) :
    QWidget(parent)
{
    setWindowTitle(tr("按鍵組"));
    myLCDNumber = new QLCDNumber;
    myLCDNumber->setDigitCount(8);    //設定位數
    layout = new QVBoxLayout(this);
    startBtn = new QPushButton;
    startBtn->setText(tr("開啟耗時計算"));
    layout->addWidget(myLCDNumber);
    layout->addWidget(startBtn);

    myTimer = new QTimer(this);
    myTimer->start(1000);             //以1000毫秒為周期起動定時器
    showTime();

    connect(myTimer,SIGNAL(timeout()),this,SLOT(showTime()));
    connect(startBtn,SIGNAL(clicked()),this,SLOT(startCalc()));
}

void Widget::showTime(){
    QTime time = QTime::currentTime();
    QString text=time.toString("hh:mm:ss"); //設定顯示時間格式
    myLCDNumber->display(text);
}

void Widget::startCalc(){
    for(int i = 0; i < 3000000000; i++){
        int a = 0;
    }
    QMessageBox::information(NULL,tr("消息框"),("計算完畢"));
}

main.cpp

#include "widget.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();

    return a.exec();
}

因為Qt中所有界面都是在UI執行緒中(也被稱為主執行緒,就是執行QApplication::exec()的線程),在這個線程中執行耗時的操作,UI就會阻塞,而讓介面停止響應,為了避免這一問題,我們改寫程式,使用QThread開啟一個新的執行緒,由這新的執行緒進行耗時的工作,而主執行緒可繼續進行使用者介面的響應。

我們新建一個WorkerThread類別,WorkerThread繼承自QThread類別,重載run()函式,run()函式內容為新執行緒要進行的工作,也就是剛剛那個耗時計算,當使用者點擊按鈕時,我們使用QThread::start()函式啟動一個執行緒,這時Qt會執行重載的那個run()函式,我們不該自行呼叫run()函式,當函式內容執行完畢時,系統會幫我們清除線程實例。這時我們可發現,現在開啟耗時運算時,介面已經不會被阻塞了。


workthread.h

#ifndef WORKTHREAD
#define WORKTHREAD

#include <QThread>

class WorkerThread : public QThread
{ 
    Q_OBJECT
public:
    WorkerThread(QObject *parent = 0) : QThread(parent){ }
protected:
    void run(){
        for(int i = 0; i < 3000000000; i++){
            int a = 0;
        }
        emit done();
    }
signals:
    void done();
};

#endif 

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QTimer>
#include <QTime>
#include <QLCDNumber>
#include <QVBoxLayout>
#include <QPushButton>
#include <QMessageBox>

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
private:
    QTimer *myTimer;
    QLCDNumber *myLCDNumber;
    QVBoxLayout *layout;
    QPushButton *startBtn;
private slots:
    void showTime();
    void finishCalc();
};

#endif 

widget.cpp

#include "widget.h"
#include "workthread.h"
Widget::Widget(QWidget *parent) :
    QWidget(parent)
{
    setWindowTitle(tr("按鍵組"));
    myLCDNumber = new QLCDNumber;
    myLCDNumber->setDigitCount(8);     //設定位數
    layout = new QVBoxLayout(this);
    startBtn = new QPushButton;
    startBtn->setText(tr("開啟耗時計算"));
    layout->addWidget(myLCDNumber);
    layout->addWidget(startBtn);

    myTimer = new QTimer(this);
    myTimer->start(1000);              //以1000毫秒為周期起動定時器
    showTime();

    WorkerThread *thread = new WorkerThread(this);
    connect(myTimer,SIGNAL(timeout()),this,SLOT(showTime()));
    connect(startBtn,SIGNAL(clicked()),thread,SLOT(start()));
    connect(thread,SIGNAL(done()),this,SLOT(finishCalc()));
}

void Widget::showTime(){
    QTime time = QTime::currentTime();
    QString text=time.toString("hh:mm:ss"); //設定顯示時間格式
    myLCDNumber->display(text);
}

void Widget::finishCalc(){
    QMessageBox::information(NULL,tr("消息框"),("計算完畢"));
}

main.cpp

#include "widget.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();

    return a.exec();
}