QtConcurrent多線程 - run()與QFuture


Qt中的 Concurrent 模塊,爲我們提供高級的(high-level)API 編寫多線程程序,而不用使用低級的(low-level)線程元語(如互斥鎖、讀寫鎖、信號量、條件變量等)。

使用的時候需要在.pro文件中添加 concurrent 模塊

QT += concurrent

我們首先來介紹一下 QtConcurrent 中最簡單的使用方法 run()


1. run()函數

Concurrent::run() 表示在一個單獨的線程中執行函數。

它的基本原型如下:
QFuture QtConcurrent::run(QThreadPool *pool, Function function, …)

  • 參數 function : 表示要在線程中執行的函數。
  • 參數 pool :線程池。表示從線程池中獲取一個線程來執行該函數。
  • 注意 :函數可能不會立即執行;一旦線程池中的線程有可用的線程時,纔會被執行。
  • 返回值 :返回一個 QFuture<T> 對象。後面會詳細說明。

關於Qt中線程池的使用,可以參考:Qt中的線程池QThreadPool

而我們更爲常用的函數是下面的這個:
QFuture QtConcurrent::run(Function function, …)
它等價於:
QtConcurrent::run(QThreadPool::globalInstance(), function, …);
表示從全局的線程池中,獲取線程。

關於 function 的說明:

  1. 函數的參數,只是做簡單的值傳遞,比如下面的例子:
extern void aFunctionWithArguments(int arg1, double arg2, const QString &string);

int integer = ...;
double floatingPoint = ...;
QString string = ...;

QFuture<void> future = QtConcurrent::run(aFunctionWithArguments, integer, floatingPoint, string);

在調用 run() 的時候,這些值會被傳遞給執行函數,之後修改這些變量的值, 比如重新設置 integer 的值,並不會對計算產生影響。

  1. 可以爲普通函數、類的成員函數、仿函數、lambda表達式。

調用const的成員函數

// call 'QList<QByteArray>  QByteArray::split(char sep) const' in a separate thread
QByteArray bytearray = "hello world";
QFuture<QList<QByteArray> > future = QtConcurrent::run(bytearray, &QByteArray::split, ',');
...
QList<QByteArray> result = future.result();

調用non-const成員函數

// call 'void QImage::invertPixels(InvertMode mode)' in a separate thread
QImage image = ...;
QFuture<void> future = QtConcurrent::run(&image, &QImage::invertPixels, QImage::InvertRgba);
...
future.waitForFinished();
// At this point, the pixels in 'image' have been inverted

使用lambda表達式:

QFuture<void> future = QtConcurrent::run([=]() {
      // Code in this block will run in another thread
  });
  ...

2. QFuture

QFuture 用來表示異步計算的結果。 QFuture 可以表示一段時間之後的異步計算的結果,使用它可以獲取和控制當前計算的狀態並獲取計算完畢之後的結果。
下面是一個簡單的例子:

extern int func(void);
int func(void) {
	// do some thing
}

QFuture<int> future = QtConcurrent::run(func);
int result = future.result();  // 該函數會阻塞等待線程執行func的計算結果返回

QFuture 常用的函數有:

  1. 獲取結果相關函數: result() 。該函數會判斷結果是否爲可用的,如果不可用則阻塞等待。如果可用則直接把結果返回。
  2. 設置運行狀態函數: cancel() 表示取消,pause() 暫停,Resumes() 恢復,這些函數在 run() 這種模式下無效。
  3. 獲取運行狀態函數: isCanceled()isStarted()isFinished()isRunning()isPaused()
  4. 進度信息: progressValue()progressMinimum()progressMaximum()progressText() ,在 run() 模式下,這些值並沒有正真的意義。waitForFinished() 會阻塞等待計算的完成,直到結果爲可用的狀態。

這裏要注意的是,QFuture<T> 模板 T 這個類型,需要有默認構造和拷貝構造。同時 QFuture 是一個基於引用計數的輕量級的類。
我們通常想要通過信號和槽的函數,異步的監控這些狀態信息。這就需要 QFutureWatcher 這個類。


3. QFutureWatcher

QFutureWatcher 使我們通過信號和槽的方式,監控 QFuture 對象。
下面是一個示例:

// Instantiate the objects and connect to the finished signal.
MyClass myObject;
QFutureWatcher<int> watcher;
connect(&watcher, SIGNAL(finished()), &myObject, SLOT(handleFinished()));

// Start the computation.
QFuture<int> future = QtConcurrent::run(...);
watcher.setFuture(future);

函數 setFuture 表示設置被監控的 QFuture 對象,爲了避免我們設置後不能及時的收到 QFuture 對象發送的信號,可以在執行 run 之前設置信號和槽的連接。
QFuture 對象的結果可用的時候, finished 信號會被 QFutureWatcher 發送。


下面是一個關於 run 使用的簡單示例,在單獨的線程中計算數值,異步通知GUI線程,這樣GUI線程就不用阻塞等待計算結果的完成。效果如下:

QtConcurrent
這是一個計算前N個數和的示例,開啓單獨線程異步計算。
爲了防止計算的太快,我這裏故意在計算函數中加了延時:

double calcTotalNumber(int number)
{
    double sum = 0.0;
    for (int i=0; i<=number; ++i)
    {
        sum += i;
        QThread::msleep(10);
    }

    return sum;
}

完整代碼如下:
頭文件:

class QtConcurrentTestWidget : public CustomWidget
{
    Q_OBJECT

public:
    QtConcurrentTestWidget(QWidget *parent = nullptr);
    ~QtConcurrentTestWidget();

private:
    QLabel* m_pResultLabel = nullptr;
    QSpinBox* m_pSpinBox = nullptr;
    QPushButton* m_pButton = nullptr;

    QFuture<double> m_future;
    QFutureWatcher<double> m_futureWatcher;

private slots:
    void onCalcButtonClicked(void);

    void threadStarted(void);
    void threadFinished(void);
};

cpp文件:


QtConcurrentTestWidget::QtConcurrentTestWidget(QWidget *parent)
    : CustomWidget(parent)
{
    // 創建界面
    QVBoxLayout* layout = new QVBoxLayout(this);
    // 顯示結果Label
    m_pResultLabel = new QLabel();
    layout->addWidget(m_pResultLabel);

    QHBoxLayout* bottomLayout = new QHBoxLayout;
    // 創建 SpinBox
    m_pSpinBox = new UICustomIntSpinBox;
    m_pSpinBox->setMinimum(1);
    m_pSpinBox->setMaximum(999999);
    m_pSpinBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
    bottomLayout->addWidget(m_pSpinBox);
    // 添加按鈕
    m_pButton = new QPushButton(tr("Calc"));
    m_pButton->setMinimumSize(60, 30);
    bottomLayout->addWidget(m_pButton);
    layout->addLayout(bottomLayout);

    QObject::connect(m_pButton, &QPushButton::clicked, this, &QtConcurrentTestWidget::onCalcButtonClicked);

    // 連接監視器的信號
    QObject::connect(&m_futureWatcher, &QFutureWatcher<double>::started, this, &QtConcurrentTestWidget::threadStarted);
    QObject::connect(&m_futureWatcher, &QFutureWatcher<double>::finished, this, &QtConcurrentTestWidget::threadFinished);
}

QtConcurrentTestWidget::~QtConcurrentTestWidget()
{

}

// 點擊計算按鈕
void QtConcurrentTestWidget::onCalcButtonClicked(void)
{
    m_pButton->setEnabled(false);
    m_future = QtConcurrent::run(calcTotalNumber, m_pSpinBox->value());
    m_futureWatcher.setFuture(m_future);
}

// 開始執行
void QtConcurrentTestWidget::threadStarted(void)
{
    m_pResultLabel->setText("Thread Started!");
}

// 執行完成
void QtConcurrentTestWidget::threadFinished(void)
{
    // 獲取結果
    double result = m_future.result();

    // 顯示結果
    QString str = "Calc Result is %1";
    str = str.arg(result);
    m_pResultLabel->setText(str);

    m_pButton->setEnabled(true);
}

作者: douzhq
個人博客主頁: 不會飛的紙飛機
微信公衆號: 不會飛的紙飛機 ,不定時更新技術文章和搞笑段子。

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