英语原文共 536 页,剩余内容已隐藏,支付完成后下载完整资料
第9章.保持对多线程的认知
在前面的章节中,我们总是设法在不依赖线程的情况下编写代码。现在是时候面对这个野兽并真正理解Qt中的线程是如何工作了。在本章中,你将开发一个显示Mandelbrot分形的多线程应用程序。这是一个繁重的并且会导致CPU内核崩溃的计算过程。
在示例项目中,用户可以看到Mandelbrot分形,通过放大图片和四处移动来发现分形的魔力。
本章涵盖以下主题:
Qt所有可行的线程技术的概述
使用2类分派工作并聚合结果
如何同步线程和最小化共享状态
低层次绘图以优化性能
常见的线程陷阱和挑战
发现QThread
Qt提供了一个复杂的线程系统。我们假设你已经了解线程基础知识和相关问题(死锁、线程同步、资源共享等等),我们将重点覆盖Qt是如何实现这些问题的。
QThread是Qt线程系统的中心类。QThread实例在程序中管理一个执行线程。
你可以继承QThread来覆盖run()函数,该函数将在QThread框架中执行。以下是如何创建和启动QThread:
start()函数调用将自动调用线程的run()函数并发出started()信号。只有在这个时候,才会创建新的执行线程。当run()完成时,线程对象将发出finished()信号。
QThread的一个基本方面:它与信号/槽机制无缝地工作。Qt是一个事件驱动的框架,其中的主事件循环(或GUI循环)处理事件(用户输入、图形化等等)以刷新UI。
每个QThread都有自己的事件循环,可以处理主循环之外的事件。如果没有被覆盖,run()调用QThread::exec()函数,它将启动线程对象的事件循环。你也可以覆盖QThread并调用自己的exec(),像这样:
只有在exec()调用时,线程事件循环才会处理started()信号。它将阻塞并等待QThread::exit()被调用。
需要注意的一件重要事情是,线程事件循环为存在于该线程中的所有Qobject交付事件。这包括在该线程中创建或移动到该线程的所有对象。这称为对象的线程关联。让我们来看一个例子:
在这段代码中,myObject是在Thread类的构造函数中构造的,该构造函数是在MainWindow中创建的。此时,线程位于GUI线程中。因此,myObject也存在于GUI线程。
注解
在QCoreApplication对象之前创建的对象没有线程关联。因此,不会向它发送任何事件。
能够在我们自己的QThread中处理信号和槽是很好的,但是我们如何在多个线程中控制信号呢?一个典型的例子是一个长时间运行的进程,它在一个单独的线程中执行,必须强调UI更新一些状态:
直觉上,我们假设第一个连接信号发送到多个线程(有结果可以在主窗口::handleResult),而第二个连接只要工作线程的事件循环。
幸运的是,由于connect()函数签名中的默认实参:连接类型,所以出现了这种情况。让我们看看完整的信号:
type关键字以Qt::AutoConnection作为默认值。让我们回顾一下Qt::ConectionType enum的可能值,就像Qt官方文档中说的:
Qt::AutoConnection:如果接收器位于发出信号的线程中,则使用Qt::DirectConnection:否则,使用Qt::QueuedConnection。连接类型是在信号发射时确定的。
Qt::DirectConnection: 当发出信号时,立即调用这个槽。槽在信令线程中执行。
Qt::QueuedConnection: 当控制返回到接收方线程的事件循环时,将调用此槽。槽在接收器的线程中执行。
Qt::BlockingQueuedConnection: 这与qt::QueuedConnection相同,除了信令线程阻塞直到槽返回。如果接收方位于信令线程中,则绝不能使用此连接,否则应用程序将死锁。
Qt::UniqueConnection: 这是一个标志,可以与前面的任何一种连接类型结合使用。当Qt::UniqueConnection被设置,QObject::connect()将会失败,如果连接已经存在(也就是说,如果相同的信号已经连接到相同的对象对相同的槽)..
当使用Qt::AutoConnection时,最终的连接类型只有在信号有效发出时才会被解析。如果你再次查看我们的示例,第一个connect():
当发出result()时,Qt将查看handleResult()线程相关性,这与result()信号的线程相关性不同。thread对象存在于MainWindow中(请记住它是在MainWindow中创建的),但是result()信号是在run()函数中发出的,该函数运行在另一个执行线程中。因此,将使用Qt::QueuedConnection槽。
现在我们来看看第二个connect():
这里,deleteLater()和finished()存在于同一个线程中;因此,将使用Qt::DirectConnection槽。
至关重要的是,你明白Qt并不关心释放对象的线程关联,它看起来只在信号“执行上下文”。
有了这些知识,我们可以再看一下我们的第一个QThread类示例,以完全理解这个系统:
当Object::started()函数被调用时,将使用Qt::QueuedConnection槽。这时你的大脑就会冻结。Thread::doWork()函数存在于Object::started()之外的另一个线程中,Object::started()是在run()中创建的。如果Thread已经在UI线程中实例化,这就是doWork()应该属于的地方。
这个系统很强大,但也很复杂。为了使事情更简单,Qt支持worker模型。它将线程通道与实际处理分开。下面是一个例子:
我们首先创建一个具有:的Worker类:
doWork()槽将包含旧的QThread::run()的内容 result()信号,它将发出结果数据
接下来,在MainWindow类中,我们创建一个简单的线程对象和一个Worker实例。worker-gt;moveToThread(thread)是魔力发生的地方。它更改worker对象的关联性。worker现在位于thread对象中。
只能将对象从当前线程推送到另一个线程。相反地,你不能拉取位于另一个线程中的对象。如果对象不在线程中,则不能更改该对象的线程相关性。一旦thread-gt;start()被执行,我们就不能调用worker-gt;moveToThread(this),除非我们是在这个新线程中执行的。
在那之后,我们执行三个connect():
我们通过在线程完成时获取它来处理worker生命周期。这个信号将使用Qt::DirectConnection。
我们在可能的UI事件上启动Worker::doWork()。这个信号将使用Qt::QueuedConnection..
3.我们在UI线程中使用handleResult()处理结果数据。这个信号将使用Qt::QueuedConnection..
总而言之,QThread既可以子类化,也可以与worker类一起使用。通常情况下,worker方法更受欢迎,因为它将线程关联通道从你希望并行执行的实际操作中分离出来。
Qt多线程技术的进阶
基于QThread, Qt中有几种线程技术。首先,要同步线程,通常的方法是使用互斥(mutex)来对给定的资源进行互斥。Qt通过QMutex类提供了它。它的用法很简单:
根据mutex.lock()指令,任何其他试图锁定互斥对象的线程都将等待直到mutex.unlock()被调用为止。
在复杂的代码中,锁定/解锁机制是容易出错的。你很容易忘记在特定退出条件下解锁互斥锁,从而导致死锁。为了简化这种情况,Qt提供了一个QMutexLocker,在需要锁住QMutex的地方使用:
当locker对象被创建时互斥锁被锁定,当locker对象被结束时解锁;例如,当它超出范围时。对于return语句出现的每一个条件,都是这样。它使代码更简单,更易于阅读。
你可能需要频繁地创建和结束线程,因为手动管理QThread实例会变得很麻烦。为此,可以使用QThreadPool类,它管理一个可重用的QThreads池。
要在由QThreadPool类管理的线程中执行代码,你将使用与我们前面介绍的worker非常接近的模式。主要区别在于处理类必须扩展QRunnable类。它看起来是这样的:
只要覆盖run()函并要求QThreadPool在单独的线程中执行QThreadPool::globalInstance()是一个静态助手函数,它允许你访问应用程序全局实例。如果需要更好地控制QThreadPool生命周期,你可以创建自己的QThreadPool。
注意,QThreadPool::start()函数接受任务的所有权,并在run()完成时自动删除它。注意,这不会像QObject::moveToThread()对worker所做的那样改变线程亲缘性!一个QRunnable类不能被重用,它必须是一个新产生的实例。
如果启动几个任务,QThreadPool会根据CPU的核心数自动分配理想的线程数。QThreadPool类可以启动的最大线程数可以通过QThreadPool::maxThreadCount()获取。
提示
如果你需要手动管理线程,但又希望基于CPU的内核数,那么可以使用方便的静态函数QThreadPool::idealThreadCount()。
另一种多线程开发方法是Qt并发框架。它是一个更高级的API,避免了互斥锁/锁/等待条件的使用,并促进了CPU核之间的处理分配。
Qt并发依赖于QFuture类来执行一个函数,并期待稍后的结果:
longRunningFunction()函数将在从默认的QThreadPool类获得的独立线程中执行。
要将参数传递给QFuture类并检索操作的结果,请使用以下代码:
这里我们将lenna作为参数传递给processGrayscale()函数。因为我们想要一个QImage作为结果,所以我们用模板类型QImage声明QFuture类。在此之后,future.result()阻塞当前线程并等待操作完成以返回最终的QImages。
为了避免阻塞,QFutureWatcher来帮助:
首先声明一个QFutureWatcher类,该类的模板参数与用于QFuture的模板参数相匹配。然后,只需将QFutureWatcher::finished信号连接到你希望在操作完成时调用的槽。
最后一步是告诉观察者对象使用watch . setfuture (future)来监视future对象。这句话看起来就像是出自科幻电影。
Qt并发还提供了MapReduce和FilterReduce实现。MapReduce是一种编程模型,它主要做两件事:
将数据集的处理过程通过映射或分布的在CPU的多个核之间。 减少或聚合结果,将其提供给调用者
这种技术最初是由谷歌推广的,它能够在一个cpu集群中处理巨大的数据集。
下面是一个简单映射操作的示例:
与QtConcurrent::run()不同,我们使用的是接受列表的映射函数,以及每次应用于不同线程中的每个元素的函数。图像列表已被修改,因此不需要使用模板类型声明QFuture。
可以使用QtConcurrent::blockingMapped()代替QtConcurrent::mapped()来阻塞操作。
最后,MapReduce操作是这样的:
这里我们添加了一个combineImage()函数,它将被映射函数processGrayscale()返回的每个结果调用。它将把中间数据inputImage合并到最终的图像中。这个函数在每个线程一次只被调用一次,所以不需要使用互斥锁来锁定结果variablemage)。
FilterReduce遵循完全相同的模式;filter函数只是允许你过滤输入列表,而不是转换它。
对Mandelbrot项目进行架构设计
本章的实例工程
剩余内容已隐藏,支付完成后下载完整资料
资料编号:[258901],资料为PDF文档或Word文档,PDF文档可免费转换为Word
以上是毕业论文外文翻译,课题毕业论文、任务书、文献综述、开题报告、程序设计、图纸设计等资料可联系客服协助查找。