QApplication线程因另一个QThread而冻结

7
在我的Qt应用程序中,我创建了一个QThread,它应该定期执行一些繁重的计算任务。主QApplication线程应该维护GUI(不包括在示例中)并定期执行一些更新操作。两个线程都有自己的计时器以启用定期的update()调用。
问题是:当工作线程的计算工作量超过某个临界值时,我的主线程停止接收计时器事件。
以下是示例代码。它会输出“Main”,表示主线程的update()方法被调用;输出“Worker”,表示工作线程的update()方法被调用。如果您运行它,您会看到“Worker”被定期打印,“Main”则只出现两次(一个在开头,一个大约在5秒后)。在完整的GUI应用程序中,这实际上意味着GUI完全冻结。
一些观察:
1. 在内部循环上设置100的限制(而不是1000)将解决问题(两个update()方法都将被定期调用)。 2. 将工作线程计时器信号的连接类型设置为Qt::DirectConnection可以解决问题。
因此,正如您所看到的,我有几种解决方法,但我希望有人向我解释原始代码存在的问题。我期望线程独立地执行它们的事件循环。我知道我通过长时间的update()操作来阻塞工作线程事件循环,但为什么会影响主线程呢?
顺便说一句,是的,我知道有QConcurrent这个替代方案。但我只是想了解一下。
test.cpp
#include <windows.h>
#include <QApplication>

#include "test.h"

HANDLE mainThread_ = INVALID_HANDLE_VALUE;
QApplication *app_ = 0;
MyObj *obj_ = 0;
MyThread *thread_ = 0;

MyObj::MyObj()
    : timer_(0)
{
    timer_ = new QTimer(0);
    connect(timer_, SIGNAL(timeout()), this, SLOT(update()));
    timer_->start(10);
}

void MyObj::update()
{
    printf("Main\n");
}

void MyThread::run()
{
    timer_ = new QTimer(0);
    connect(timer_, SIGNAL(timeout()), this, SLOT(update()));
    timer_->start(10);

    exec();
}

void MyThread::update()
{
    printf("Worker\n");

    // do some hard work
    float f = 0.f;
    for (int i=0; i < 100000; ++i)
    {
        for (int j=0; j < 1000; ++j)
        {
            f += i * j;
        }
    }
}

int main()
{
    int argc = 0;
    app_ = new QApplication(argc, 0);

    obj_ = new MyObj();
    thread_ = new MyThread();
    thread_->start();

    QApplication::exec();

    return 0;
}

test.h

#include <QTimer>
#include <QThread>

class MyObj : public QObject
{
    Q_OBJECT

public:
    MyObj();

public slots:
    void update();

private:
    QTimer *timer_;
};

class MyThread : public QThread
{
    Q_OBJECT

public:
    void run();

public slots:
    void update();

private:
    QTimer *timer_;
};

更新:我收到了一些来自尊敬的成员的答案(请在下面阅读)。现在我想澄清哪个错误的想法破坏了我的代码。

如您所见,计划是拥有两个线程,每个线程定期运行一些update()过程。我的错误是认为update()只是一个过程,而它是一个。特定对象的槽具有其自己的线程关联,这意味着其主体将在该线程中执行(除非使用Qt :: DirectConnection分派信号)。现在,看起来我已经用计时器做得很好——它们中的每一个都属于不同的线程——但在update()方面搞砸了。因此,我最终在主线程中执行了两个update()程序。显然,在某些时候,事件循环会被计时器事件淹没,并且永远不会完成迭代。

至于解决方案。如果您已经阅读了“你错了”(确实应该),则知道在未从QThread子类化而是单独创建并附加到QThread的对象中实现所有逻辑相当方便。就个人而言,我认为从QThread子类化并没有什么错误,但请记住,您的对象仅控制���程而不属于该线程。因此,这不是您希望在该线程中执行的代码的位置。

2个回答

3
这里的第一个问题是你从QThread继承,正如这里所述,“你做错了”。
你遇到的问题源于线程亲和性(对象在哪个线程上运行)。例如,如果你从QThread继承并在构造函数中创建对象,而没有将对象设置为父对象,则该对象将在主线程中运行,而不是新线程中。因此,在MyThread构造函数中,你应该有:
MyThread::MyThread()
    : timer_(0)
{
    timer_ = new QTimer(0);
    connect(timer_, SIGNAL(timeout()), this, SLOT(update()));
    timer_->start(10);
}

这里的定时器(timer_)将在主线程上运行,而不是新线程。 为了避免重复,我之前的一个回答在这里解释了线程亲和性。

解决问题的最佳方法是将您的类更改为继承自QObject,然后将该对象移动到一个新线程中。


谢谢。实际上,我已经多次阅读了所有这些帖子,例如“你做错了”。但是只有现在我才开始明白。因此,我的MyThread对象驻留在主线程中,并且发送到它的计时器事件进入主线程的事件循环。很酷。现在,我想知道为什么主线程的update()几乎从不有机会?我的意思是两个计时器具有相等的间隔,它们应该并行运行。 - Anton Zherzdev
你可以在MyObject和MyThread的不同点(比如构造函数、run和update函数)中打印线程地址,这将更好地帮助你了解与线程亲和性有关的情况。只需调用printf("0x%x", obj->thread())。但总体而言,如果你只是重构而不继承QThread,你很可能会看到它按预期工作。 - TheDarkKnight
作为“你做错了”的文章的补充,这是一个关于如何使用线程的好文章:http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/ - TheDarkKnight
我正要将您的帖子标记为答案,但仔细阅读后发现有些误解。您说得好像是定时器的亲和性出了问题,但实际上并非如此。我的定时器没问题。是MyThread :: update()插槽错位了。我以为它会在定时器所属的线程中执行,但自然而然地它是在MyThread所属的线程(主线程)中执行的。 - Anton Zherzdev
2
@AntonZherzdev 如果您计划使用槽函数,那么您永远不应该对QThread进行子类化。正如您所注意到的,槽函数将在您的“MyThread”对象所在的线程中执行,而不是在它正在管理的线程中执行。文档中也明确说明了这一点。在QThread子类中实现新的槽函数容易出错且不被鼓励。 - thuga
显示剩余3条评论

2
首先...http://blog.qt.digia.com/blog/2010/06/17/youre-doing-it-wrong/ 从你的主函数中,我看不到任何类似于GUI的东西...你只是为了某种原因调用了QApplication::exec()而不是app_->exec() (?)
对于你的线程问题: 你可以创建一个继承自QObject的类,它具有doUpdate()或类似的slot。这样你就可以像这样做:
TheUpdateObject* obj = new TheUpdateObject;
obj->moveToThread(thread_);  // thread_ is a QThread object
thread_->start();
connect(thread_, SIGNAL(finished()), obj, SLOT(deleteLater()));
QTimer* tmr = new QTimer(this);
tmr->setTimeout(10);
connect(tmr, SIGNAL(timeout()), obj, SLOT(doUpdate()));
connect(tmr, SIGNAL(timeout()), tmr, SLOT(start()));
tmr->start();

所以计时器应该重新启动,并且doUpdate()应该在另一个线程中被调用。
在GUI内部,您不需要检查更新,qt框架应该在需要时重新绘制。

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