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 的值,并不会对计算产生影响。

(2) 可以为普通函数、类的成员函数、仿函数、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线程就不用阻塞等待计算结果的完成。效果如下:

这是一个计算前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);
}
不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:QtConcurrent多线程 - map、mapped和mappedReduced