QThread执行冻结了我的GUI

时间:2012-07-11 05:52:24

标签: multithreading qt

我是多线程编程的新手。我用Qt编写了这个简单的多线程程序。但是当我运行这个程序时它冻结了我的GUI,当我点击我的寡妇时,它会回应你的程序没有响应。 这是我的widget类。我的线程开始计算一个整数,当这个数字可以被1000分割时发出它。在我的小部件中,我只是用信号槽机制捕获这个数字,并在标签和进度条中显示它。

   Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    MyThread *th = new MyThread;
    connect( th, SIGNAL(num(int)), this, SLOT(setNum(int)));
    th->start();
}


void Widget::setNum(int n)
{
    ui->label->setNum( n);
    ui->progressBar->setValue(n%101);
}

这是我的线程run()函数:

void MyThread::run()
{
    for( int i = 0; i < 10000000; i++){
        if( i % 1000 == 0)
            emit num(i);
    }
}

谢谢!

3 个答案:

答案 0 :(得分:9)

问题在于你的线程代码产生了一个事件风暴。循环计数非常快 - 如此之快,以至于每1000次迭代发出一个信号这一事实非常重要。在现代CPU上,执行1000个整数分区大约需要10微秒IIRC。如果环路是唯一的限制因素,那么您将以每秒约100,000次的峰值速率发射信号。事实并非如此,因为其他因素限制了表现,我们将在下面讨论。

让我们了解当您在接收器QObject所在的不同线程中发出信号时会发生什么。信号打包在QMetaCallEvent中并发布到接收线程的事件队列中。在接收线程中运行的事件循环(这里是GUI线程)使用QAbstractEventDispatcher的实例对这些事件进行操作。每个QMetaCallEvent都会调用已连接的广告位。

接收GUI线程的事件队列的访问由QMutex序列化。在Qt 4.8及更高版本中,QMutex实现获得了很好的加速,因此每个信号发射导致锁定队列互斥的事实不太可能成为问题。唉,事件需要在工作线程的堆上分配,然后在GUI线程中解除分配。如果线程碰巧在不同的内核上执行,那么当快速连续发生时,许多堆分配器的性能都很差。

最大的问题出在GUI线程中。似乎有一堆隐藏的O(n ^ 2)复杂度算法!事件循环必须处理10,000个事件。这些事件很可能非常快速地传递,最终在事件队列中的连续块中。事件循环必须在处理其他事件之前处理所有事件。调用插槽时会发生许多昂贵的操作。不仅QMetaCallEvent从堆中释放,而且标签调度update()(重绘),并且在内部将可压缩事件发布到事件队列。在最坏的情况下,可压缩事件发布必须遍历整个事件队列。这是一个潜在的O(n ^ 2)复杂行为。在实践中可能更重要的另一个此类操作是进度条的setValue内部调用QApplication::processEvents()。这可以递归调用您的插槽以从事件队列传递后续信号。你做的工作比你想象的要多,这就锁定了GUI线程。

检测您的插槽并查看是否以递归方式调用它。一种快速而肮脏的方式是

void Widget::setNum(int n)
{
  static int level = 0, maxLevel = 0;
  level ++;
  maxLevel = qMax(level, maxLevel);
  ui->label->setNum( n);
  ui->progressBar->setValue(n%101);
  if (level > 1 && level == maxLevel-1) {
    qDebug("setNum recursed up to level %d", maxLevel);
  }
  level --;
}

什么是冻结你的GUI线程不是QThread的执行,而是你做GUI线程的大量工作。即使您的代码看起来无害。

关于processEvents和Run-to-Completion Code

的附注

我认为让QProgressBar::setValue调用processEvents()是一个非常糟糕的主意。它只会鼓励用户编写代码的方式(连续运行代码而不是简短的运行完成代码)。由于processEvents()调用可以递归到调用者,setValue成为一个不受欢迎的角色,可能非常危险。

如果想要以连续样式编码并保持运行完成语义,那么有很多方法可以在C ++中处理它。一个是通过利用预处理器,例如代码见my other answer

另一种方法是使用expression templates来使C ++编译器生成所需的代码。您可能希望在此处利用模板库 - Boost spirit具有适当的实现起点,即使您没有编写解析器,也可以重复使用。

Windows Workflow Foundation还解决了如何编写顺序样式代码的问题,并将其作为短的运行完成片段运行。他们诉诸于指定XML中的控制流程。显然没有重用标准C#语法的直接方法。它们只提供数据结构,a-la JSON。如果想要的话,在Qt中实现XML和基于代码的WF都很简单。尽管.NET和C#提供了对编程生成代码的充分支持,但所有这些......

答案 1 :(得分:2)

您实现线程的方式,它没有自己的事件循环(因为它不调用exec())。我不确定run()中的代码是否实际在您的线程内或GUI线程内执行。

通常你不应该继承QThread。您可能这样做是因为您阅读了Qt文档,遗憾的是仍然建议继承QThread - 尽管开发人员很久以前曾写过a blog entry声明您不应该继承QThread。不幸的是,他们仍然没有适当更新文档。

我建议在Qt Blog上阅读"You're doing it wrong",然后使用answer by "Kari"作为如何设置基本多线程系统的示例。

答案 2 :(得分:0)

But when I run this program it freezes my GUI and when I click inside my window, it responds that your program is not responding.

是的,因为IMO你正在在线程中做太多工作耗尽CPU 。当进程在处理应用程序事件队列请求时没有显示进度时,通常会弹出program is not responding消息。在你的情况下会发生这种情况。

所以在这种情况下你应该找到一种划分工作的方法。仅举例来说,线程以 100 的块运行并重复线程直到它完成 10000000

另外,当您执行冗长的操作时,您应该查看QCoreApplication::processEvents()