Qt中的多线程技术详解

Qt中的多线程技术

Qt提供了许多用于处理线程的类和函数。 以下是Qt程序员可以用来实现多线程应用程序的四种不同方法。


QThread:带有可选事件循环的低级API

QThread是Qt中所有线程控制的基础。 每个QThread实例表示并控制一个线程。
QThread可以直接实例化或派生子类。 实例化QThread提供了一个并行事件循环,允许在辅助线程中触发QObjectslots。 继承QThread则允许应用程序在启动其事件循环之前初始化新线程,或者在没有事件循环的情况下运行并行代码。

示例:

//TestThread.h
 class TestThread : public QThread
 {
     Q_OBJECT
 private:
     void run();
 };
//TestThread.cpp
 void TestThread::run()
 {
      qDebug() << "这是在一个新的线程里: " << thread()->currentThreadId();
 }
 //main.cpp
int main(int argc, char *argv[])
 {
     QCoreApplication app(argc, argv);
     TestThread thread;
     thread.start();
     qDebug() << "这是在GUI主线程里:" << app.thread()->currentThreadId();
     thread.wait();  // 在thread运行后才可以停止。
     return 0;
 }

QThreadPool和QRunnable:重用线程

经常创建和销毁线程可能非常的耗费资源。 为了减少这种开销,可以重用现有的线程来执行新的任务。QThreadPool是可重用的QThreads的集合,用来管理并重新利用QThreads
要在QThreadPool所管理的的线程之一中运行代码,需要继承QRunnable并重新实现QRunnable::run()。 然后使用QThreadPool::start()将刚刚实现的QRunnable放入QThreadPool的运行队列中。 当QThreadPool中存在空闲的线程时,QRunnable::run()中的代码将在该线程中执行。
每个Qt应用程序都有一个全局线程池,可通过QThreadPool::globalInstance()访问。 此全局线程池根据CPU中的内核数量自动维护最佳线程数。 不过我们仍然可以显式创建和管理一个的QThreadPool

class TestTask : public QRunnable
{
    void run()
    {
        qDebug() << "这里子线程" << QThread::currentThread();
    }
};
TestTask *test = new TestTask();
// QThreadPool 持有test对象并自动delete
QThreadPool::globalInstance()->start(test);

Qt Concurrent:高级API

Qt Concurrent模块提供了处理一些常见并行计算模式的高级函数:

  • map
  • filter
  • reduce

与使用QThreadQRunnable不同,这些功能不需要使用低级线程原语,如互斥锁或信号量。相反,它们返回一个QFuture对象,可用于在功能准备就绪时检索结果。 QFuture也可用于查询执行进度并暂停/恢复/取消执行。为方便起见,QFutureWatcher通过信号和槽实现与QFutures的交互。

Qt Concurrentmapfilterreduce算法会自动将计算分配到所有可用的处理器内核中,因此现在编写的应用程序在以后部署在具有更多CPU核的的系统上时仍然能充分利用硬件。
该模块还提供了QtConcurrent::run()函数,该函数可以在另一个线程中运行任何函数。但是,QtConcurrent::run()仅对 map,filter,reduce提供部分支持。 QFuture可用于检索函数的返回值并检查线程是否正在运行。但是,对QtConcurrent::run()的调用只使用一个线程,不能暂停/恢复/取消,并且无法查询进度。
这得通过另一篇文章说明了,详见Qt Concurrent详解


WorkerScript: QML中的多线程

WorkerScript QML类型允许JavaScript代码与GUI线程并行运行。
每个WorkerScript实例可以附加一个.js脚本。 当调用WorkerScript::sendMessage()时,脚本将在单独的线程(以及单独的QML上下文)中运行。 当脚本完成运行时,它可以将返回值发送回GUI线程,该线程将调用WorkerScript::onMessage()信号处理程序。
使用WorkerScript与使用已移动到另一个线程的QObject工作者类似(即通过QObject::moveToThread)。 数据通过信号在线程之间传输。

//main.qml
Rectangle {
    width: 300; height: 300
    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) {
    // ... 耗时的操作在这里执行
    WorkerScript.sendMessage({ 'reply': 'Mouse is at ' + message.x + ',' + message.y })
}

选择一个适当的方法实现多线程

如上所述,Qt为开发多线程应用程序提供了不同的解决方案。 给定应用程序的正确解决方案取决于新线程的目的和线程的生命周期。 以下是Qt的多线程技术比较,接下来会针对某些示例用例的推荐解决方案。

技术比较

特色 QThread QRunnable 和 QThreadPool QtConcurrent::run() Qt Concurrent (Map, Filter, Reduce) Qt Concurrent (Map, Filter, Reduce)
Language C++ C++ C++ C++ QML
可设定线程优先级 Yes Yes
线程可以处理一个事件循环 Yes
线程可以从信号中获取数据更新 Yes(通过一个Worker对象) Yes(由WorkerScript获得)
线程可以用信号控制 Yes(由QThread获得) Yes (通过QFutureWatcher获得)
线程可以被QFuture监测 部分 Yes
天生具有暂停/恢复/取消的功能 Yes

最佳实践

生命周期 操作 解决方案
一次调用 在另一个线程中运行一个新的函数,在运行期间可以获取进度。 Qt提供不同的解决方案派生QThread并重新实现QThread::run()然后启动。并且发出进度更新的信号。派生QRunnable:run(),并使用QThreadPool::globalInstance()->start(QRunnable)来运行。 将进度写入一个线程安全的变量来给外部进度信息。使用QtConcurrent::run()运行函数。 将进度写入一个线程安全的变量来给外部进度信息。
一次调用 在另一个线程中运行一个现有函数并获取其返回值。 使用 QtConcurrent::run()运行函数。 当函数返回时,让QFutureWatcher发出finished()信号,然后调用QFutureWatcher::result()来获取函数的返回值。
一次调用 使用所有可用的核心对容器的所有项目执行操作。 例如,从图像列表中生成缩略图。 使用Qt ConcurrentQtConcurrent::filter()函数选择容器元素,并使用QtConcurrent::map()函数将操作应用于每个元素。要将输出整合为单个结果,请改为使用QtConcurrent::filteredReduced()QtConcurrent::mappedReduced()
一次调用/持久运行 在纯QML应用程序中执行耗时计算,并在结果准备好时更新GUI。 将计算代码放在.js脚本中并将其附加到WorkerScript实例。 调用sendMessage()以在新线程中启动计算。 让脚本也调用WorkerScript::sendMessage(),将结果传递回GUI线程。 在onMessage中处理结果并在那里更新GUI。
持久运行 让一个对象在另一个线程中生存,可以根据请求执行不同的任务,并且/或者可以接收新的数据来处理 子类化QObject以创建一个工作者(Worker)。 实例化这个工作对象和一个QThread。 将工作对象移至新线程(运用moveToThread)。 通过队列的信号-槽连连接(queued signal-slot connections,connect函数的最后一个参数)向工作对象发送命令或数据。
持久运行 在另一个线程中重复执行耗时的操作,线程不需要接收任何信号或事件。 子类话QThread并重新实现QThread::run(),直接写入无限循环。 在没有事件循环的情况下启动线程。 让线程发出信号将数据发送回GUI线程。
持久运行 生存在另一个线程中的对象,执行诸如轮询端口等重复的任务并与GUI线程通讯。 同上,但是在工作线程中使用一个定时器来轮询。尽管如此,处理轮询的最好的解决方案是彻底避免它。有时QSocketNotifer是一个替代。

你可能感兴趣的:(Qt,QML)