QThread与std::thread的区别

18

我看到了关于“pthread vs std::thread”和“QThread vs pthread”的不同话题,但没有关于“std::thread vs QThread”的。

我必须编写一个软件来驱动3D打印机并需要使用线程。将会有一个线程不断检查安全性,另一个执行打印过程,一些用于分别驱动每个硬件组件(移动,喷头等)的线程,等等...... 该程序是使用C++11/Qt为Windows开发的。

起初我想使用QThread,但在我看来,QThread似乎不像std::thread那样允许您做很多事情例如,当我阅读Anthony Williams的《C++ Concurrency in Action》时,发现可以通过类似于std :: thread t1(&Class :: function,this,...)的方式要求std :: thread从另一个线程中执行函数,而这似乎在QThread中不可能。

我最想拥有的机制是一种方法,可以说出我想要在当前线程还是在另一个线程中执行一个函数。

你会选择哪一个并为什么?


3
除非被迫,我不想将我的 打印代码 与特定的 GUI库 绑定。我认为尽可能使用 std::thread 没有任何缺点。 - Galik
2
或者,如果某人的开发环境不支持std::thread,比如旧版本的MSVC,那么Qt程序可能是源代码可移植的,而std::thread应用程序则不是。 - infixed
5
Qt不仅是一个GUI库,更是一个应用程序开发框架,GUI只是其中的一小部分。使用Qt的其余部分代替std::stuff来处理核心逻辑并没有什么问题,例如,Qt容器的速度和易用性都与std::容器相当甚至更好,因为它们的使用不那么冗长。 - dtech
2
@Galik 谢谢你的回答。我可以想象为什么你会这样编程。在我的情况下,知道我已经使用Qt作为核心逻辑,你认为我使用std::thread而不是qthread会有什么优势吗? - ElevenJune
3
@ElevenJune 只有您自己知道您的需求。考虑一件事情,那就是构建一个代码库,可以在不同的项目中重复使用,这些项目可能不会始终使用特定的库,这样可以使未来的开发更快/更便宜。出于这个原因,我倾向于首先在标准的C++库中寻找解决方案。您必须决定在此实例中使用Qt所带来的好处。 - Galik
显示剩余4条评论
3个回答

21

QThread不仅仅是一个线程,它还是一个线程管理器。如果您想让您的线程与Qt一起运行,那么使用QThread就可以了。Qt是事件驱动的,和现代编程语言一样。这比“使线程运行函数”要复杂和灵活得多。

在Qt中,通常您会创建一个工作线程和QThread,然后将工作线程移动到该线程,然后由于工作对象的事件系统调用的每个函数都将在相应线程中执行。

因此,您可以将功能封装在不同的工作对象中,例如SafetyCheckerPrinterServoDriverJetDriver等,为每个对象创建实例,将其移动到专用线程中即可。您仍然可以调用会“阻塞”的函数,而不是使用细粒度事件,并使用原子或信号量进行线程间同步。只要不阻塞主/ GUI线程,这样做就没有问题。

您可能不希望将打印机代码设计为事件驱动的,因为在多线程场景中,这将涉及到排队连接,这比直接连接甚至虚拟调度慢得多。所以,如果您将多线程细化得太多,您可能实际上会遇到巨大的性能问题。

话虽如此,使用Qt的非GUI功能也有其优点。它将使您更轻松地设计出更清洁和灵活的设计,并且如果您正确实现事物,则仍将获得多线程的好处。您仍然可以使用事件驱动的方法来管理整个过程,这比仅使用std::thread要容易得多,后者是一个更低级别的构造。您可以使用事件驱动的方式来设置、配置、监控和管理设计,而关键部分可以在辅助线程中执行阻塞函数,以实现最低可能同步开销的细粒度控制。

澄清一下 - 这篇答案不侧重于异步任务执行,因为其他两个答案已经涉及到了,并且正如我在评论中提到的那样,异步任务并不是真正用于控制应用程序的。它们适合执行小任务,这些任务仍需要比你想要阻塞主线程更长的时间。作为一个推荐的指南,所有需要超过25毫秒的任务最好都以异步方式执行。而打印则可能需要数分钟甚至数小时,并意味着在并行和使用同步的情况下连续运行控制函数。异步任务无法为控制应用程序提供性能、延迟和顺序保证。


谢谢你的回答。你所说的关于封装工作线程并将它们移动到QThread中的方法,我已经做过了:例如,对于ServoDriver,我只需要调用ServoDriver :: moveTo(x,y),移动就在另一个线程中完成,不会阻塞程序的其他部分。现在的问题是:我想先移动再打印一些东西,我该如何等待移动完成后再打印? - ElevenJune
听起来你需要实现一个简单的指令队列。然后将命令插入其中。工作线程执行一个命令,完成后执行下一个,直到整个队列被处理完毕。每个命令可以由一个整数表示,后跟其参数,QDataStream 是实现它的好选择。 - dtech
我也尝试过,它可以工作,但是我似乎无法在同一个应用程序中同时拥有两种行为。现在我所做的是,可以同时分别驱动每个组件。这对于校准GUI非常有用:您说“移动到那里”,它就会移动,GUI不会被阻塞,非常好。为此,我调用ServoDriver :: moveTo(x,y),它向表示我要移动的元素的类发送信号(仅针对移动有4个不同的元素)。该类在不同线程上具有信号处理程序并在该线程中执行移动。使用该实现,我无法做到您所说的。 - ElevenJune
从技术上讲,您不会想在桌面操作系统中编写硬件控制应用程序,因为它实际上将在专用微控制器上运行,配备实时操作系统,甚至可能没有操作系统,这样可以保证实时性能。 - dtech
1
在您的特定情况下,您将向命令队列中输入命令,解释器将读取这些命令并将它们分派给适当的工作程序,例如moveToXY将由操作码int和x、y int表示,解释器将读取操作码,在switch语句中切换到moveXY的情况,读取两个参数并将操作发送到ServoDriver,当驱动程序完成后,它将通知解释器读取下一个命令,该命令将被发送到其相应的对象,依此类推... - dtech

12
< p > < code >std::thread 和 QThread 的主要问题是,它们实现了字面意思:为您创建一个线程,这个线程可能只会做一件事情。使用std::thread并发运行函数非常浪费资源,因为线程是昂贵的资源,所以仅为运行某个函数而创建一个线程通常是过度kill的。

std::thread t1(&Class::function, this, ...)看起来很不错,但通常是过早地进行优化,并将其作为“同时执行任务”的通用方法进行推荐不太妥当。有更好的方式。

< ol >
  • 如果要在工作线程中同时运行函数/函数对象/方法,请使用QtConcurrent::runstd::async

    QtConcurrent::run默认使用默认线程池,您也可以传递自己的QThreadPool实例。典型情况下,将默认线程池用于CPU密集型任务,例如计算、图像转换、渲染等,并使用专用的更大的I/O线程池来执行由于API限制而阻塞的操作(例如,许多数据库库只提供阻塞API,因为它们的设计本质上是有缺陷的)。例如:

    // interface
    QThreadPool * ioPool();
    
    // implementation
    Q_GLOBAL_STATIC(QThreadPool, ioPool_impl);
    QThreadPool * ioPool() { return ioPool_impl; }
    
  • 如果您想让一个 QObject 存活在另一个线程中(可能与其他对象共存),请使用 QThread,然后使用 moveToThread 将您的对象移到该线程。

    从工作线程发出信号以线程安全地将数据传递到主线程是一种惯用法。例如,假设您希望拥有一个响应灵敏的 GUI 并希望在工作线程中从磁盘加载图像:

  • class MyWidget : public QWidget {
      QLabel m_label;
      ...
      Q_SIGNAL void setImage(const QImage &);
     public:
      MyWidget() {
       ...
       connect(MyWidget, &MyWidget::setImage, this, [this](const QImage & image){
        m_label.setPixmap(QPixmap::fromImage(image));
       });
       QtConcurrent::run(ioPool(), [this]{ setImage({"/path/to/image.png"});  });
      }
    };
    

    1
    我怀疑QtConcurrent::run不适合用于像驱动打印机这样的任务。 - dtech
    1
    @ddriver 如果您使用I/O池,只要打印机可以从另一个线程使用,那就完全没问题。 - Kuba hasn't forgotten Monica
    1
    非常清晰的回复,我研究了QtConcurrent,它看起来很棒。谢谢! - ElevenJune

    8

    QThread很适合将线程集成到Qt系统中(例如需要发出信号或连接到某些槽)。

    尽管QThread的布局仍然是为“旧”c++设计的。你必须创建一个类和所有这些开销(代码和输入方面),只是为了在一个线程中运行。

    如果你只是想简单地启动一个线程,我认为c++11的std::thread更少冗长/要写的代码。你可以使用lambda或函数指针,并且可以提供任意数量的参数。因此,对于简单的线程处理,我建议使用c++11线程而不是QThreads。

    当然,这可能是个人喜好的问题,你可以自行选择。


    尽管Qt有几种不同的高级线程对象,但C++没有。如果您需要其中一些对象而不是基本线程,或者您根本不需要基本线程,则可以考虑这些对象。

    如果需要等待事物发生,可以像使用QThreadPool或简单地使用QTimer这里有关于在Qt中使用替代方法来代替裸线程的阅读资料。

    QConcurrent也更接近于c++11的futureasync,并且还具有可选的线程池可以运行。


    4
    我认为std::thread接受functor/lambda是一个糟糕的接口,因为它鼓励创建暂时的线程来运行短小的代码片段。 OP甚至也掉进了这个陷阱!而QThread的API则告诉你,线程绝不是轻量级的东西。在实践中,std::threadQThread相比没有更多的开销,因为真正的开销是由操作系统创建线程所需的成本。无论您将什么代码放入包装它的帮助类中都是无关紧要的。 - Kuba hasn't forgotten Monica
    5
    在大多数情况下,您应该使用 std::asyncQtConcurrent::run,而不是 QThreadstd::threadQThread 主要用于将 QObject 移动到特定线程中。 - Kuba hasn't forgotten Monica
    @KubaOber,“overhead”指的是您需要输入/编写的打字或代码量,而不是性能方面。但我应该澄清这一点。但我同意。在大多数情况下,您应该更喜欢使用更高级别的API。 - Hayt
    我认为异步执行的意图更适合执行大量小任务而不阻塞主线程/ GUI线程,但它不适用于控制应用程序,这意味着在外部硬件处理完成时需要连续执行。 - dtech
    就像我之前所说的,大多数情况下,当你有一个线程需要处理IO操作,并且你只需要一个线程时,使用线程池可能会过度设计,这取决于你需要与对象交互的程度等因素。一切都取决于上下文。 - Hayt

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接