Qt 学习之路:线程总结

前面咱们已经详细介绍过有关线程的一些值得注意的事项。如今咱们开始对线程作一些总结。

有关线程,你能够作的是:git

  • QThread子类添加信号。这是绝对安全的,而且也是正确的(前面咱们已经详细介绍过,发送者的线程依附性没有关系)


不该该作的是:
github

  • 调用moveToThread(this)函数
  • 指定链接类型:这一般意味着你正在作错误的事情,好比将QThread控制接口与业务逻辑混杂在了一块儿(而这应该放在该线程的一个独立对象中)
  • QThread子类添加槽函数:这意味着它们将在错误的线程被调用,也就是QThread对象所在线程,而不是QThread对象管理的线程。这又须要你指定链接类型或者调用moveToThread(this)函数
  • 使用QThread::terminate()函数

不能作的是:设计模式

  • 在线程还在运行时退出程序。使用QThread::wait()函数等待线程结束
  • QThread对象所管理的线程仍在运行时就销毁该对象。若是你须要某种“自行销毁”的操做,你能够把finished()信号同deleteLater()槽链接起来

那么,下面一个问题是:我何时应该使用线程?浏览器

首先,当你不得不使用同步 API 的时候。安全

若是你须要使用一个没有非阻塞 API 的库或代码(所谓非阻塞 API,很大程度上就是指信号槽、事件、回调等),那么,避免事件循环被阻塞的解决方案就是使用进程或者线程。不过,因为开启一个新的工做进程,让这个进程去完成任务,而后再与当前进程进行通讯,这一系列操做的代价都要比开启线程要昂贵得多,因此,线程一般是最好的选择。服务器

一个很好的例子是地址解析服务。注意咱们这里并不讨论任何第三方 API,仅仅假设一个有这样功能的库。这个库的工做是将一个主机名转换成地址。这个过程须要去到一个系统(也就是域名系统,Domain Name System, DNS)执行查询,这个系统一般是一个远程系统。通常这种响应应该瞬间完成,可是并不排除远程服务器失败、某些包可能会丢失、网络可能失去连接等等。简单来讲,咱们的查询可能会等几十秒钟。网络

UNIX 系统上的标准 API 是阻塞的(不只是旧的gethostbyname(3),就连新的getservbyname(3)getaddrinfo(3)也是同样)。Qt 提供的QHostInfo类一样用于地址解析,默认状况下,内部使用一个QThreadPool提供后台运行方式的查询(若是关闭了 Qt 的线程支持,则提供阻塞式 API)。异步

另一个例子是图像加载和缩放。QImageReaderQImage只提供了阻塞式 API,容许咱们从设备读取图片,或者是缩放到不一样的分辨率。若是你须要处理很大的图像,这种任务会花费几十秒钟。socket

其次,当你但愿扩展到多核应用的时候。ide

线程容许你的程序利用多核系统的优点。每个线程均可以被操做系统独立调度,若是你的程序运行在多核机器上,调度器极可能会将每个线程分配到各自的处理器上面运行。

举个例子,一个程序须要为不少图像生成缩略图。一个具备固定 n 个线程的线程池,每个线程交给系统中的一个可用的 CPU 进行处理(咱们可使用QThread::idealThreadCount()获取可用的 CPU 数)。这样的调度将会把图像缩放工做交给全部线程执行,从而有效地提高效率,几乎达到与 CPU 数的线性提高(实际状况不会这么简单,由于有时候 CPU 并非瓶颈所在)。

第三,当你不想被别人阻塞的时候。

这是一个至关高级的话题,因此你如今能够暂时不看这段。这个问题的一个很好的例子是在 WebKit 中使用QNetworkAccessManager。WebKit 是一个现代的浏览器引擎。它帮助咱们展现网页。Qt 中的QWebView就是使用的 WebKit。

QNetworkAccessManager则是 Qt 处理 HTTP 请求和响应的通用类。咱们能够将它看作浏览器的网络引擎。在 Qt 4.8 以前,这个类没有使用任何协助工做线程,全部的网络处理都是在QNetworkAccessManager及其QNetworkReply所在线程完成。

虽然在网络处理中不使用线程是一个好主意,但它也有一个很大的缺点:若是你不能及时从 socket 读取数据,内核缓冲区将会被填满,因而开始丢包,传输速度将会直线降低。

socket 活动(也就是从一个 socket 读取一些可用的数据)是由 Qt 的事件循环管理的。所以,阻塞事件循环将会致使传输性能的损失,由于没有人会得到有数据可读的通知,所以也就没有人可以读取这些数据。

可是什么会阻塞事件循环?最坏的答案是:WebKit 本身!只要收到数据,WebKit 就开始生成网页布局。不幸的是,这个布局的过程很是复杂和耗时,所以它会阻塞事件循环。尽管阻塞时间很短,可是足以影响到正常的数据传输(宽带链接在这里发挥了做用,在很短期内就能够塞满内核缓冲区)。

总结一下上面所说的内容:

  • WebKit 发起一次请求
  • 从服务器响应获取一些数据
  • WebKit 利用到达的数据开始进行网页布局,阻塞事件循环
  • 因为事件循环被阻塞,也就没有了可用的事件循环,因而操做系统接收了到达的数据,可是却不能从QNetworkAccessManager的 socket 读取
  • 内核缓冲区被填满,传输速度变慢

网页的总体加载时间被自身的传输速度的下降而变得愈来愈坏。

注意,因为QNetworkAccessManagerQNetworkReply都是QObject,因此它们都不是线程安全的,所以你不能将它们移动到另外的线程继续使用。由于它们可能同时有两个线程访问:你本身的和它们所在的线程,这是由于派发给它们的事件会由后面一个线程的事件循环发出,但你不能肯定哪一线程是“后面一个”。

Qt 4.8 以后,QNetworkAccessManager默认会在一个独立的线程处理 HTTP 请求,因此致使 GUI 失去响应以及操做系统缓冲区过快填满的问题应该已经被解决了。

那么,什么状况下不该该使用线程呢?

定时器

这多是最容易误用线程的状况了。若是咱们须要每隔一段时间调用一个函数,不少人可能会这么写代码:

当读过咱们前面的文章以后,可能又会引入线程,改为这样的代码:

最好最简单的实现是使用定时器,好比QTimer,设置 1s 超时,而后将doWork()做为槽:

咱们所须要的就是开始事件循环,而后每隔一秒doWork()就会被自动调用。

网络/状态机

下面是一个很常见的处理网络操做的设计模式:

在通过前面几章的介绍以后,不用多说,咱们就会发现这里的问题:大量的waitFor*()函数会阻塞事件循环,冻结 UI 界面等等。注意,上面的代码尚未加入异常处理,不然的话确定会更复杂。这段代码的错误在于,咱们的网络实际是异步的,若是咱们非得按照同步方式处理,就像拿起枪打本身的脚。为了解决这个问题,不少人会简单地将这段代码移动到一个新的线程。

一个更抽象的例子是:

这段抽象的代码与前面网络的例子有“殊途同归之妙”。

让咱们回过头来看看这段代码到底是作了什么:咱们实际是想建立一个状态机,这个状态机要根据用户的输入做出合理的响应。例如咱们网络的例子,咱们实际是想要构建这样的东西:

以此类推。

既然知道咱们的实际目的,咱们就能够修改代码来建立一个真正的状态机(Qt 甚至提供了一个状态机类:QStateMachine)。建立状态机最简单的方法是使用一个枚举来记住当前状态。咱们能够编写以下代码:

source对象是哪来的?这个对象其实就是咱们关心的对象:例如,在网络的例子中,咱们可能但愿把 socket 的QAbstractSocket::connected()或者QIODevice::readyRead()信号与咱们的槽函数链接起来。固然,咱们很容易添加更多更合适的代码(好比错误处理,使用QAbstractSocket::error()信号就能够了)。这种代码是真正异步、信号驱动的设计。

将任务分割成若干部分

假设咱们有一个很耗时的计算,咱们不能简单地将它移动到另外的线程(或者是咱们根本没法移动它,好比这个任务必须在 GUI 线程完成)。若是咱们将这个计算任务分割成小块,那么咱们就能够及时返回事件循环,从而让事件循环继续派发事件,调用处理下一个小块的函数。回一下如何实现队列链接,咱们就能够轻松完成这个任务:将事件提交到接收对象所在线程的事件循环;当事件发出时,响应函数就会被调用。

咱们可使用QMetaObject::invokeMethod()函数,经过指定Qt::QueuedConnection做为调用类型来达到相同的效果。不过这要求函数必须是内省的,也就是说这个函数要么是一个槽函数,要么标记有Q_INVOKABLE宏。若是咱们还须要传递参数,咱们须要使用qRegisterMetaType()函数将参数注册到 Qt 元类型系统。下面是代码示例:

因为没有任何线程调用,因此咱们能够轻易对这种计算任务执行暂停/恢复/取消,以及获取结果。

至此,咱们利用五个章节将有关线程的问题简单介绍了下。线程应该说是所有设计里面最复杂的部分之一,因此这部份内容也会比较困难。在实际运用中确定会更多的问题,这就只能让咱们具体分析了。

相关文章
相关标签/搜索