QThread执行冻结了我的GUI

s4e*_*eed 3 qt multithreading

我是多线程编程的新手.我用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 Code Online (Sandbox Code Playgroud)

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

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

谢谢!

Rei*_*ica 9

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

让我们理解当你在接收器所在的不同线程中发出信号时会发生什么QObject.信号打包在一个QMetaCallEvent并发布到接收线程的事件队列中.在接收线程中运行的事件循环 - 这里是GUI线程 - 使用实例来处理这些事件QAbstractEventDispatcher.每个都会QMetaCallEvent调用连接的插槽.

接收GUI线程的事件队列的访问由a序列化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 --;
}
Run Code Online (Sandbox Code Playgroud)

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

关于processEvents和Run-to-Completion Code的附注

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

如果想要以连续样式编码并保持运行完成语义,那么有很多方法可以在C++中处理它.一个是通过利用预处理器,例如代码看到我的另一个答案.

另一种方法是使用表达式模板来使C++编译器生成所需的代码.您可能希望在此处利用模板库 - 即使您没有编写解析器,Boost精灵也有一个可以重用的实现的起点.

Windows工作流基础也解决了如何编写顺序代码风格尚未有它运行的是短期到完成碎片的问题.他们诉诸于指定XML中的控制流程.显然没有重用标准C#语法的直接方法.它们只提供数据结构,a-la JSON.如果想要的话,在Qt中实现XML和基于代码的WF都很简单.尽管.NET和C#提供了充足的代码生成代码支持,但所有这些......