计算机系统应用教程网站

网站首页 > 技术文章 正文

多线程Qt下的八条规则 qt多线程直接处理数据

btikc 2024-10-24 09:31:30 技术文章 8 ℃ 0 评论

相信资深Qter都认识Giuseppe D’Angelo,这位有着二十多年Qt开发经验,Qt源码行数贡献的最多的开发者之一,同时也是Qt项目的审批者,所说话的份量不言而喻。

原文作者: 朱塞普 D 安吉洛(Giuseppe D’Angelo

多线程一直是一个老生常谈的话题,而Qt作为C++生态非常重要的一环,熟练使用多线程是必备的技能。

Qt中封装了很多多线程的轮子,我们在日常开发中,除了要学会怎么用之外,如何用好才是关键。

下面来看一下可以代表Qt官方的Giuseppe D’Angelo是如何告诫大家的。

尽管多线程的概念看起来非常简单,但在日常编码中,使用多线程总会遇到难以复现和追踪的恶心BUG。这导致我们使用多线程写出可靠代码是非常困难的。接下来让我们深入了解一下,为什么会出现这种状况。

首先你需要对框架底层、编程语言、编译器的有深层次的理解,这样你才能知道如何避免线程高发问题;其次,你还要理解同步原语,使用合适的设计模式来编写多线程代码,只有这样才能使多业务在所有条件下正常执行;最后,你还要学会使用调试工具来调试多线程,只有这样你才能发现多线程内部难以复现的BUG。

当涉及到Qt多线程后,尤其需要了解你的框架和设计模式。Qt能让你写出令人惊奇的多线程程序,也能让你开枪打自己的脚。多年的Qt框架、Qt客户端多线程BUG查找和修复经验磨练了我们多线程编码水平,这里有几条我们总结出来的顶级信条,它可以让你避免多线程常见问题,可以让你的程序在第一时间正常运行。

1. 避免使用QThread::sleep()

尽管 QThread::sleep() 是一个让你的线程休眠的API,如果你使用了它,那么你应该重新学习一下事件驱动设计。如果你将“多线程休眠”改为“多线程事件等待”(或者最好是不使用多线程),那么你将会节省巨大的系统资源,不然这些资源就被空转的线程浪费了。QThread::sleep() 用作计时器,也是一个糟糕的设计,因为从线程休眠到返回控制的这段时间,是很难受到控制的。理论上讲,当进程终止时,休眠的线程也会导致问题。前台线程可以阻止程序终止,直到他们被唤醒,而后台线程永远不会唤醒前台线程,结果就是程序的清理终止一直被阻止。

2. 禁止子线程操作GUI

QtGUI 操作不是线程安全的,所以非主线程外一切线程对GUI的操作都是不安全的。这就意味着窗口小部件、QtQuickQPixmap,以及其他任何类的对象都不能在非主线程操作GUI。当然也有些特例:GUI函数只操作数据,不触及窗口UI管理,这是可以被子线程调用的,比如 QImageQPainter。但是依旧要小心,像 QBitmapQPixmap 这些类依旧是线程不安全的。可以查询每个 API 的官方文档:如果你没有看到 可重入(reentrant 标识,那么在子线程的一切调用都是不安全的。

3. 不要阻塞主线程

不要在主线程调用任何不明确阻塞时间的函数(比如 QThread::wait() ),因为这些函数会使你主线程的事件循环暂停,造成UI冻结。如果你的主线程阻塞时间足够长,那么操作系统会认为你的程序冻结,会询问你要不要干掉它(鲲哥: Windows常见于 “窗口未响应” )。不管是无限阻塞还是长时间阻塞主线程,对程序来说都是很搓的做法。

4. 一定要在拥有QObjects所有权的线程上对其进行销毁

Qt从设计上就不允许在非拥有QObject 所有权的线程上对其进行销毁,这意味这,一个QThread在销毁之前,它所掌握的所有QObject必须先销毁。如果没有这么做,那么很可能引发数据完整问题,比如常见的内存泄露和进程 crash
那么如何确定销毁
QObject 的线程就是拥有其所有权的线程呢?我们可以将其创建为在QThread::run() 方法函数内执行的自动变量,关联QThread::finished() 信号与QThread::deleteLater() 槽函数;也可以通过moveToThread()QObject 移动到其他线程进行延迟析构。注意,一旦你把QObject从原来拥有它的线程移动到新线程后,你就不能用原来的线程操作它了,因为它的所有权属于新的线程了。

5. 线程同步的时候,不要相信自己的直觉

一种非常常见的设计方式就是一个线程把自己的运行状态,通过布尔变量的形式传递给监控线程,供其监控。一个简单的数据结构,一个线程写,另一个线程读,这样看起来是不需要做同步保护,因为我们能确保最终能读到它,是这样吗?直觉上看起来是没问题的,实际上就是这么简单一个案例也是线程不安全的。
C++标准规定线程同步是强制性的,任何超纲的行为都会导致未定义行为(
UB)。如果你并没有做线程同步,即使是一个简单案例,也会给你制造问题。事实上,Linux内核发现的一些严重BUG就是这么产生的。我们最应该做的不是过度思考安全与否,假设有一个被多个线程同时操作的数据,即使这个数据看起来多么人畜无害,产生问题的几率有多么小,我们一定要用合适同步手段来保护这个数据。

6. 假设QObject是不可重入的

一个函数能被不同的线程不同数据同时访问,且不需要任何同步方法保护,那么这个函数就是可重入的。Qt官方文档显示QObject是可重入的,但实际上却有很多警告:

  • 基于事件的类型是不可重入的(定时器、socket等等)
  • 假设某一QObject正在其归属的线程中进行事件派发,如果你在另一个线程操作它,这就可能会导致竞争。
  • 从属于同一父子关系树的所有QObject对象,必须在同一个线程进行处理。
  • 删除QThread 对象之前,必须先把其拥有的所有QObject对象干掉。
  • 你必须在一个对象归属的线程里面使用对这个对象使用 moveToThread()

为了避免以上所有特殊场景,最简单的方法就是认为QObject 是不可重入的。事实上,这就意味着你必须在QObject 所属的线程上操作它。这也会让你远离所有可能会出现问题,同时也是难以察觉的场景。

7. 避免往QThread增加槽函数

因为QThread 对象和创建他们的线程存在关联(或许不存在关联),但在子线程使用信号槽的时候,很容易产生问题。尽管子线程可以使用槽函数,当需要规避大量难以察觉的缺陷时,我们建议你不要这么做。
如果你不需要重写
QThread::run() 函数,那么请不要从QThread 派生子类。只需要创建一个QThread 对象即可,这样你就能避免槽函数问题(更多请关注作者博客的其他博文)。

8. 请使用C++标准库,因为更好用

最后,不管是C++标准库,还是第三方库,在多线程方面都比Qt更具特色。它们包含Qt没有的功能——并行算法、协程、锁存器、内存屏障、原子智能指针、任务延续、executors、并发队列、分布式计数器等等。
Qt的多线程在一些场景下仍然好用,比如C++标准库里面没有线程池,但是Qt就有。好消息就是C++标准库和Qt完美兼容,你可以自由地将标准库引入你的代码里面。事实上,除非你要用线程操作
QObject 对象才必须用到QThread,不然使用C++标准库还是Qt的多线程库,依赖于你个人喜好。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表