Qt提供了许多用于处理线程的类和函数,我们可以在从其中选择一种合适的来实现。总结下来一共有4种:
- QThread
-
QThreadPool and QRunnable
-
Qt Concurrent
-
WorkerScript (QML)
下面就通过示例来展示每种方式的使用。示例是参考Qt帮助文档,在Qt5.9.8 MSVC2015下运行,没有考虑线程同步的情况。
一、QThread的使用
QThread是Qt中所有线程控制的基础。每个QThread实例表示并控制一个线程。QThread的使用有2种方式:
- 继承QThread,重写run。
- QObject::moveToThread。
1.继承QThread,重写run
使用这种方式的时候,QThread实例存在于实例化它的旧线程中,而run()在新线程中执行。这意味着QThread的槽都将在旧线程中执行。如下面代码中的WorkerThread::thread接口,打印的是构造WorkerThread的线程,而run里面打印的新的线程。即使我通过connect接口连接这个槽也是一样的,比如MainWindow中有一个按钮,把按钮的clicked信号的workerThread的thread连接起来。
class WorkerThread : public QThread
{
Q_OBJECT
public:
WorkerThread(QObject *parent);
public slots:
void printThread();
void stop();
signals:
void resultReady(const QString &result);
// QThread interface
protected:
virtual void run() override;
private:
bool m_stop;
};
WorkerThread::WorkerThread(QObject *parent)
: QThread (parent)
, m_stop(false)
{
}
void WorkerThread::printThread()
{
qDebug() << "Thread1:" << currentThread();
}
void WorkerThread::stop()
{
m_stop = true;
}
void WorkerThread::run()
{
qDebug() << "Thread2:" << currentThread();
while (!m_stop) {
static int i = 0;
emit resultReady(QString::number(i++));
msleep(1000);
}
}
///
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
WorkerThread *workerThread = new WorkerThread(this);
connect(workerThread, &WorkerThread::resultReady, this, &MainWindow::handleResults);
workerThread->start();
workerThread->thread();
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::handleResults(const QString &result)
{
qDebug() << "result:" << result;
}
2.QObject::moveToThread
在这种方式中,Worker的槽函数中的代码将在一个单独的线程中执行。但是,您可以自由地将Worker的槽函数连接到任何线程中任何对象的任何信号。由于队列连接的机制,跨不同线程连接信号和槽是安全的。在下面的这个示例中,MainWindow::MainWindow中打印的线程和Worker::thread中打印的线程是不一样的。也就是说Worker::thread中的代码是在另外一个线程中执行的。
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr);
public slots:
void thread();
};
Worker::Worker(QObject *parent) : QObject(parent)
{
}
void Worker::thread()
{
qDebug() << QThread::currentThread();
}
/
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QThread *workerThread = new QThread(this);
Worker *worker = new Worker;
worker->moveToThread(workerThread);
connect(ui->pushButton, &QPushButton::clicked, worker, &Worker::thread);
connect(workerThread, &QThread::finished, worker, &QObject::deleteLater);
qDebug() << QThread::currentThread();
workerThread->start();
}
二、QThreadPool and QRunnable
频繁地创建和销毁线程可能代价高昂。为了减少这种开销,可以将现有线程重新用于新任务。QThreadPool是可重复使用的QThread的集合。
要在QThreadPool的某个线程中运行代码,请重新实现QRunnable::run()并实例化子类QRunnable。使用QThreadPool::start()将QRunnable放入QThreadPool的运行队列中。当一个线程可用时,QRunnable::run()中的代码将在该线程中执行。
每个Qt应用程序都有一个全局线程池,可以通过QThreadPool::globalInstance()访问。该全局线程池根据CPU中的内核数量自动维护最佳线程数量。但是,可以显式创建和管理单独的QThreadPool。(自Qt帮助文档)
在下面的示例代码中,MainWindow::MainWindow中打印的线程和HelloWorldTask中打印的线程不一样。
class HelloWorldTask : public QRunnable
{
void run()
{
qDebug() << QThread::currentThread();
}
};
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
HelloWorldTask *hello = new HelloWorldTask();
qDebug() << QThread::currentThread();
// QThreadPool takes ownership and deletes 'hello' automatically
QThreadPool::globalInstance()->start(hello);
}
三、Qt Concurrent
QtConcurrent命名空间提供了高级API,可以在不使用诸如互斥、读写锁、等待条件或信号量等低级线程原语的情况下编写多线程程序。使用QtConcurrent编写的程序会根据可用的处理器内核数量自动调整使用的线程数量。这意味着,今天编写的应用程序在未来部署在多核系统上时将继续扩展。(自Qt帮助文档)
1.QtConcurrent::map()
QFuture<void> map(Sequence &sequence, MapFunction function)
QFuture<void> map(Iterator begin, Iterator end, MapFunction function)
将一个函数应用于容器中的每个项,并将运算结果替换原来的元素。
static void add(int &a)
{
a = a + 2;
}
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QList<int> v = {1, 2, 3, 4, 5};
qDebug() << v;
QFuture<void> result = QtConcurrent::map(v, add);
result.waitForFinished();
qDebug() << v;
}
输出:
(1, 2, 3, 4, 5)
(3, 4, 5, 6, 7)
2.QtConcurrent::mapped()
QFuture<T> mapped(const Sequence &sequence, MapFunction function)
QFuture<T> mapped(ConstIterator begin, ConstIterator end, MapFunction function)
功能类似 map() 函数,它会返回一个新容器存储函数处理后的结果。
static int add(const int &a)
{
return a + 2;
}
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QList<int> v = {1, 2, 3, 4, 5};
qDebug() << v;
QFuture<int> result = QtConcurrent::mapped(v, add);
result.waitForFinished();
qDebug() << v;
qDebug() << result.results();
}
//
输出:
(1, 2, 3, 4, 5)
(1, 2, 3, 4, 5)
(3, 4, 5, 6, 7)
3.QtConcurrent::mappedReduced()
QFuture<T> mappedReduced(const Sequence &sequence, MapFunction mapFunction, ReduceFunction reduceFunction, QtConcurrent::ReduceOptions reduceOptions = UnorderedReduce | SequentialReduce)
QFuture<T> mappedReduced(ConstIterator begin, ConstIterator end, MapFunction mapFunction, ReduceFunction reduceFunction, QtConcurrent::ReduceOptions reduceOptions = UnorderedReduce | SequentialReduce)
功能类似 mapped() 函数,按顺序为每个项目调用mapFunction一次。每个mapFunction的返回值被传递给reduceFunction。
static int add(const int &a)
{
return a + 2;
}
static void addSum(int &a, const int &b)
{
a = a + b;
}
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QList<int> v = {1, 2, 3, 4, 5};
qDebug() << v;
QFuture<int> result = QtConcurrent::mappedReduced(v, add, addSum);
result.waitForFinished();
qDebug() << result.result();
}
输出:
(1, 2, 3, 4, 5)
25
4.QtConcurrent::filter()
QFuture<void> filter(Sequence &sequence, FilterFunction filterFunction)
根据筛选函数的结果从容器中删除所有项目。
static bool isOdd(const int &a)
{
return a % 2 != 0;
}
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QList<int> v = {1, 2, 3, 4, 5};
qDebug() << v;
QFuture<void> result = QtConcurrent::filter(v, isOdd);
result.waitForFinished();
qDebug() << v;
}
输出:
(1, 2, 3, 4, 5)
(1, 3, 5)
5.QtConcurrent::filtered()
QFuture<T> filtered(const Sequence &sequence, FilterFunction filterFunction)
QFuture<T> filtered(ConstIterator begin, ConstIterator end, FilterFunction filterFunction)
功能类似filter(), 将结果以新的容器返回。
static bool isOdd(const int &a)
{
return a % 2 != 0;
}
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QList<int> v = {1, 2, 3, 4, 5};
qDebug() << v;
QFuture<int> result = QtConcurrent::filtered(v, isOdd);
result.waitForFinished();
qDebug() << v;
qDebug() << result.results();
}
/
输出:
(1, 2, 3, 4, 5)
(1, 2, 3, 4, 5)
(1, 3, 5)
6.QtConcurrent::filteredReduced()
QFuture<T> filteredReduced(const Sequence &sequence, FilterFunction filterFunction, ReduceFunction reduceFunction, QtConcurrent::ReduceOptions reduceOptions = UnorderedReduce | SequentialReduce)
QFuture<T> filteredReduced(ConstIterator begin, ConstIterator end, FilterFunction filterFunction, ReduceFunction reduceFunction, QtConcurrent::ReduceOptions reduceOptions = UnorderedReduce | SequentialReduce)
功能类似filtered,把结果处理成一个单一的值。
static void addSum(int &a, const int &b)
{
a = a + b;
}
static bool isOdd(const int &a)
{
return a % 2 != 0;
}
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QList<int> v = {1, 2, 3, 4, 5};
qDebug() << v;
QFuture<int> result = QtConcurrent::filteredReduced(v, isOdd, addSum);
result.waitForFinished();
qDebug() << result.result();
}
/
输出:
(1, 2, 3, 4, 5)
9
上面的每个接口都有一个blockingXXX的接口,比如blockingMap、blockingMapped。这些接口将被阻止,直到处理完序列中的所有项目为止。
7.QtConcurrent::run()
QFuture<T> run(Function function, ...)
QFuture<T> run(QThreadPool *pool, Function function, ...)
第一个接口等价
QtConcurrent::run(QThreadPool::globalInstance(), function, ...);
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QtConcurrent::run([]{
qDebug() << "11111111111";
});
QtConcurrent::run(std::bind(&MainWindow::handleResults, this, "Test"));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::handleResults(const QString &result)
{
qDebug() << "result:" << result;
}
//
输出:
11111111111
result: "Test"
四、WorkerScript
WorkerScript QML类型允许JavaScript代码与GUI线程并行运行。
每个WorkerScript实例都可以附加一个.js脚本。当调用WorkerScript::sendMessage()时,该脚本将在一个单独的线程(和一个独立的QML上下文)中运行。当脚本完成运行时,它可以向GUI线程发送一个回复,GUI线程将调用WorkerScript::onMessage()信号处理程序。使用WorkerScript类似于使用QObject::moveToThread。数据通过信号在线程之间传输。
import QtQuick 2.9
import QtQuick.Window 2.2
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Text {
id: myText
text: 'Click anywhere'
}
WorkerScript {
id: myWorker
source: "script.js"
onMessage: myText.text = messageObject.reply
}
MouseArea {
anchors.fill: parent
onClicked: myWorker.sendMessage({ 'x': mouse.x, 'y': mouse.y })
}
}
//script.js
WorkerScript.onMessage = function(message) {
// ... long-running operations and calculations are done here
WorkerScript.sendMessage({ 'reply': 'Mouse is at ' + message.x + ',' + message.y })
}
当用户单击矩形内的任何位置时,将调用sendMessage(),从而触发script.js中的WorkerScript.onMessage()处理程序。这反过来会发送一条回复消息,然后由myWorker的onMessage()处理器接收。
由于WorkerScript.onMessage()函数是在单独的线程中运行的,因此JavaScript文件是在独立于主QML引擎的上下文中的。这意味着,与导入QML的普通JavaScript文件不同,上例中的script.js无法访问QML项的属性、方法或其他属性,也无法通过QQmlContext访问QML对象上设置的任何上下文属性。
此外,可以传递给脚本和从脚本传递的值的类型也有限制。有关详细信息,请参阅sendMessage()文档。
五、选择合适的方法
如上所述,Qt为开发线程应用程序提供了不同的解决方案。给定应用程序的正确解决方案取决于新线程的用途和线程的使用寿命。下面是Qt的线程技术的比较,然后是一些示例用例的推荐解决方案。
六、用例示例
线程周期 |
操作 |
解决方案 |
一次调用 |
在另一个线程中运行一个新的线性函数,可以选择在运行过程中更新进度。 |
Qt提供了不同的解决方案: 将函数放在重新实现的QThread::run()中,然后启动QThread。发出信号以更新进度。或 将函数放在重新实现的QRunnable::run()中,并将QRunnable添加到QThreadPool中。写入线程安全的变量以更新进度。或 使用QtConcurrent::Run()运行函数。写入线程安全的变量以更新进度。 |
一次调用 |
在另一个线程中运行现有函数并获取其返回值 |
使用QtConcurrent::Run()运行函数。当函数返回时,让QFutureWatcher发出finished()信号,并调用QFutureWatcher::result()来获取函数的返回值。 |
一次调用 |
使用可使用的所有的核对一个容器所有项执行操作。例如,从图像列表中生成缩略图。 |
使用QtCurrent的QtCurrent::filter()函数来选择容器元素,使用QtConcurrent::map()函数对每个元素应用一个操作。要将输出合并为一个结果,请改用QtConcurrent::filteredReduced()和QtConcCurrent::mappedReduced()。 |
一次调用/永久 |
在纯QML应用程序中执行长时间计算,并在结果准备就绪时更新GUI。 |
将计算代码放在.js脚本中,并将其附加到WorkerScript实例。调用sendMessage()在新线程中启动计算。让脚本也调用WorkerScript::sendMessage(),将结果传递回GUI线程。在onMessage中处理结果,并在那里更新GUI。 |
永久 |
一个QObject存在另一个线程中,该线程可以根据请求执行不同的任务和/或接收新的数据进行处理。 |
对QObject进行子类化以创建worker。实例化此worker对象和一个QThread。将worker移动到新线程。通过队列连接的信号槽连接向工作对象发送命令或数据。 |
永久 |
在另一个线程中重复执行复杂的操作,该线程不需要接收任何信号或事件。 |
直接在重新实现QThread::run()的中编写无限循环。启动线程,不需要事件循环。让线程发出信号,将数据发送回GUI线程。 |
第五和第六来自Qt的版本文档Thread Support in Qt。