KDAB:多线程Qt的八个规则

Qt中多线程的最大优点

尽管多线程的概念可能很简单,但是带有线程的代码会导致一些严重的错误,几乎不可能重现或跟踪这些错误。这使得使用线程编写防弹代码成为高阶。让我们更深入地了解为什么。

首先,您需要比一般的框架,语言和编译器内部知识更好的知识,才能知道如何避免出现线程问题。其次,您需要了解同步原语和适当的设计模式,以便创建可以在所有条件下正常运行的多线程代码。最后,您需要了解如何将调试工具与多个线程一起使用,以便能够找到棘手的问题来重现多线程错误中固有的问题。

当涉及到Qt和多线程时,尤其需要知道您的框架和设计模式。Qt使您能够制作出色的多线程应用程序-并为之腾飞。多年来,我们通过在Qt框架和Qt客户端代码中查找并修复线程错误来磨练我们的多线程专业知识。以下是我们为避免Qt应用首次正常运行而避免的最常见陷阱的主要规则的简短列表:

1.永远不要调用QThread :: sleep()

尽管有一个API可以让您的线程进入睡眠状态,但是它就是QThread :: sleep()-如果要调用它,则应该真正考虑事件驱动的设计。通过将“休眠线程”更改为“等待事件的线程”(或者更好的是,根本没有线程),您将节省大量系统资源,这些资源否则会被空闲线程浪费。QThread :: sleep()也不利于计时,因为返回控制之前所花费的时间很少受到限制。从理论上讲,休眠线程也可能导致应用程序终止时出现问题。前台线程可以防止应用程序在唤醒之前终止,而后台线程可能永远不会重新唤醒,从而阻止了干净的最终确定。

2.切勿在主线程上进行GUI操作

Qt的GUI操作不是线程安全的,因此非主线程无法安全地执行任何GUI操作。这意味着没有窗口小部件,QtQuick,QPixmap或任何涉及窗口管理器的东西。有一些例外:仅在第二个线程上调用仅处理数据但不触摸窗口管理器的GUI函数,例如QImage和QPainter。但是要小心,因为像QBitmap或QPixmap这样的类实际上并不安全。检查每个API的文档:如果在文档顶部没有看到说明该函数是可重入的注释,则从主线程中调用该函数是不安全的。

3.不要阻塞主线程

不要在主线程上调用任何可能阻塞未指定时间的函数(例如QThread :: wait())。由于这些功能使主线程停止运行,因此所有事件处理都将暂停,并且UI会冻结。如果您等待足够长的时间,则OS应用程序管理器会认为您的应用程序已冻结,并询问用户是否要杀死它-并不酷。两者都是不友好应用程序的配方。

4.始终销毁拥有它们的线程上的QObject

Qt不允许您从不拥有它的任何线程中销毁QObject。这意味着在销毁QThread之前,必须先销毁线程拥有的所有QObject。未能正确清理会导致数据完整性问题,例如流行的内存泄漏和/或崩溃。

您如何确保正确的线程是破坏您的QObject的线程?可以将其创建为QThread的run()方法内的自动变量,将QThread :: finished()连接QObject :: deleteLater(),或者通过使用moveToThread()将QObject移至另一个线程来延迟销毁请注意,一旦将QObject移离拥有它的线程,就无法再使用该线程触摸它。您必须使用拥有该对象的新线程。

5.同步时不要相信您的直觉

一种非常常见的设计模式是,一个线程通常通过写入监视线程可以轮询的布尔状态变量来向监视线程发送其状态信号。有了一个单词的数据结构,只有一个线程写入它,只有一个线程读取它,看来这种情况实际上并不需要并发保护,因为可以保证读取最终会发生,对吗?实际上,即使是这种简单的情况也不是安全的。

C ++标准指出,线程同步是强制性的,任何超出规范的行为都可能导致未定义的行为。如果您不同步,即使是在“简单”情况下,您也要自找麻烦。实际上,在Linux内核中已经发现了一些严重的错误,情况与此处所述完全相同。最好的办法是不要过分考虑安全与否–如果有多个线程同时访问同一数据,那么无论它们引起问题的可能性有多么小,都可以通过适当的同步机制来保护它们。

6.就像QObject是不可重入的

可重入功能意味着只要不同的线程处理不同的数据,就可以安全地使用它而无需同步。Qt文档表明QObject是可重入的,但是对此重入有很多警告:

  • 基于事件的类不能重入(计时器,套接字等)
  • 给定QObject的事件分派发生在与之关联的线程中;如果您从另一个线程触摸对象,可能会导致Qt内的竞争
  • 同一父/子树中的所有QObject必须具有相同的线程亲缘关系
  • 您必须先删除线程拥有的所有QObject,然后再删除QThread
  • 您只能从对象具有亲和力的线程上对对象调用moveToThread()

为了避免所有这些特殊情况,通常更容易地像QObject重入那样行事实际上,这意味着您只应在拥有它的线程上触摸QObject。这将使您远离所有可能引起麻烦的非显而易见的极端情况。

7.避免向QThread添加插槽

由于QThread对象与创建它们的原始线程具有亲和力,并且(也许是不直观的)对其自身不具有亲和力,因此当尝试在非主线程上使用信号和插槽时,这会引起问题。尽管可以完成非主线程使用插槽的设计,但是由于它需要回避很多非显而易见的陷阱,因此我们建议您避免使用这种设计。

如果不需要重写QThread:run(),则根本不要继承QThread的子类。只需创建它的一个实例,就可以避免插槽问题(有关如何使用worker的内容,请参阅此博客文章末尾的链接,我的演讲)。

8.如果更自然,请使用标准库线程

最后,C ++标准库和其他第三方库都具有不属于Qt的各种线程类-并行算法,协程,闩锁,屏障,原子智能指针,延续,执行程序,并发队列,分布式柜台之类的。

Qt的多线程功能在某些情况下仍然更好:例如,Qt具有线程池,而C ++标准却没有。好消息是C ++类都与Qt兼容,并且可以自由地合并到您的代码中。实际上,除非线程操作QObjects并且必须使用Qt线程,否则可以根据自己的喜好使用C ++或Qt线程类。


如果您喜欢这个简短的摘要,则可能需要观看我在QtCon上发表的完整QThread演讲,或查看有关此主题的演示幻灯片这些深入探究了这些规则背后的原因,并为需要做的和不做的提供了代码示例。

https://www.kdab.com/the-eight-rules-of-multithreaded-qt/

 

关于“多线程Qt的八个规则”的10条思考

  1. 我对#6(规则lgtm的其余部分)感到困惑。您说“就像QObject是不可重入的那样操作”,然后说“实际上,这意味着您应该只触摸拥有它的线程上的QObject”。在我看来,“仅在拥有它的线程上触摸QObject”才是可重入的意思。如果它是不可重入的,那么我只能在_one_线程和_only_一个线程上使用该不可重入的* type *(例如,如果类具有静态数据成员)。此外,您声称“基于事件的类是不可重入的(计时器,套接字等),但Qt文档却说:“在多线程应用程序中,可以在具有事件循环的任何线程中使用QTimer”。如果QTimer是不可重入的,则情况并非如此:您只能在单个线程(可能是主线程)上实例化QTimer类。

    我完全同意“您只应在拥有它的线程上触摸QObject”,但不要认为这转化为“ act [ing]好像QObject是不可重入的”。

    我想念什么吗?

    1. 朱塞佩·德安吉洛

      你好 让我详细说明一下我的意思。

      在我看来,“仅在拥有它的线程上触摸QObject”才是可重入的意思。

      函数重入的Qt定义是,可以由多个任意线程同时在不同数据上调用该函数,而无需同步。该函数可以由同一数据上的多个线程调用,但是需要外部同步(也就是说,用户负责同步)。您可以在这里找到定义确实要当心,这与文献中其他地方所用的定义不一样!

      将相同的Qt定义扩展到类:如果可以安全地同时从多个任意线程的不同对象上调用其成员函数(方法),而无需同步,则该类是可重入的;同时在同一对象上调用方法需要用户进行同步。

      例如,根据这个定义,QString是可重入的:不同的线程可以同时在不同的QString对象上调用QString函数,而无需任何同步。但是,如果您想同时在多个线程中使用同一QString对象,则需要自己添加同步。

      我的观点是QObject没有遵循该定义。即使您确实同步了对同一个QObject的访问,也完全不允许您从与该对象的相似性线程不同的线程对QObject进行某些操作-这些操作将失败/吐出警告/崩溃/等。(强调从严格意义上讲,如果您不使用这些操作,那么同步确实可以工作;这就是QObject在其文档中被标记为可重入的原因。)

      简而言之:我的意思是,从一般意义上说,QObject违反了上述“可重入”的定义;因此,更容易被认为是不可重入的。

      如果它是非可重入的,那么我只能在_one_线程和_only_一个线程上使用该非可重入的* type *

      一旦我们进入非可重入区域,Qt中就会有两个主要子类别。我没有好名字,所以我不会尝试定义,但是基本上是:

      1. 对象只能在一个线程和一个线程中使用的类(例如,GUI类,只能在主线程中使用)
      2. 只能在一个线程和一个线程中使用其对象的类,但该线程取决于单个实例,并且有可能更改(=> QObject)

      因此,我认为您实际上是在指2。您指的是1.。而且都错

      (对于nitpick,1.实际上实际上是使用全局资源(例如,到显示服务器的连接)的这些类的副产品,而没有任何同步。由于您在主线程中打开了该连接,因此将所有这些类固定在主线程)。

      最后,

      如果QTimer是不可重入的,则情况并非如此:您只能在单个线程(可能是主线程)上实例化QTimer类。

      这是上述类型2的另一个不可重入的情况。

      希望这能澄清一些事情,感谢您的评论!

      1. 好的 您说的是添加同步不允许您从不属于它们的线程访问QObject。我同意,这是有道理的。但是,这是“可重入”一词的特殊用法:具有不同数据的不同线程可以同时调用相同的方法(这意味着_is_可重入:-P)。

        您说的是“行为”,好像它们不是可重入的:但是这样做意味着仅从单个线程访问特定的“类型”。

        无需进一步解释,我了解我现在为什么感到困惑……但是您可能需要重新措辞一下,以使内容更清楚。

        1. 朱塞佩·德安吉洛

          没问题。我认为混乱的根源只是“可重入”一词的定义稍有不同。我在“ Qt意义”上使用它,但这不是通用的。

  2. 嗨,仅从工作线程读取QT UI数据(未经修改)是否安全?假设我有一个带有lineEdit小部件的登录表单,用户必须在其中输入密码。然后,我阻止(禁用Windows)并生成一个线程来验证密码(例如,这需要时间)。这个线程可以直接从小部件读取密码吗?

    如果没有,那么我相信我必须在GUI线程上动态分配包含密码的QString,将其与GUI线程分离,将其附加在工作线程中,最后删除工作线程上的QString对象?

    1. 朱塞佩·德安吉洛

      你好

      尽管它“可能”起作用,但官方的回答是“不”,您不允许这样做。如您所建议,您可以只读取行编辑的内容(作为QString),并在生成它时将其提供给线程。我对“分离”部分感到非常困惑,您实际上并不需要这个部分,只需将字符串复制或移动到线程中即可,仅此而已:

      QString password = lineEdit->text();
      auto thread = QThread::create( [password]() { use(password); } );
      thread->start();
      
      1. 谢谢。我想到了一种C样式的指针指向线程参数的传递。确实,C ++ lambda是正确的方法。

  3. 再见,

    感谢您的帖子,非常有趣和有用。这8条规则很明确,但是我对如何实现要在我的应用程序中并行运行的后台任务有疑问:更详细地讲,我有Gui程序,并且希望在后台检索一些信息(例如,FTP中的文件)服务器或REST调用),并且一旦检索到新数据,主应用程序就会显示一个弹出窗口以告知有关此信息。为了避免持续轮询外部资源,我想“暂停”并检查每隔X分钟是否有新数据可用。您认为Worker / QThread方法是最佳解决方案吗?还是有更好的?

    提前致谢。

    1. 朱塞佩·德安吉洛

      你好

      我绝对会尝试避免出现您的情况下的线程;定期触发检查的QTimer应该绰绰有余。

      1. 好的,我尝试过使用QTimer,您是对的,它很容易,而且这种方法避免了多线程管理的任何可能的问题。非常感谢。

posted @ 2021-01-06 18:56  findumars  Views(760)  Comments(0Edit  收藏  举报