3

我是多线程编程的新手。我用 Qt 编写了这个简单的多线程程序。但是当我运行这个程序时,它会冻结我的 GUI,当我在我的寡妇内部单击时,它会响应您的程序没有响应。这是我的小部件类。我的线程开始计算一个整数,并在这个数字可被 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);
    }
}

谢谢!

4

3 回答 3

10

问题在于您的线程代码产生了事件风暴。循环计数非常快——如此之快,以至于每 1000 次迭代发出一个信号这一事实几乎无关紧要。在现代 CPU 上,进行 1000 次整数除法需要 10 微秒 IIRC。如果循环是唯一的限制因素,您将以每秒约 100,000 个的峰值速率发出信号。情况并非如此,因为性能受到其他因素的限制,我们将在下面讨论。

让我们了解当您在接收器所在的不同线程中发出信号时会发生什么QObject。信号被封装在一个 a 中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 代码的旁注

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

如果一个人想要以连续的风格编写代码,同时保持运行到完成的语义,那么在 C++ 中有一些方法可以解决这个问题。一种是利用预处理器,例如代码见我的另一个答案

另一种方法是使用表达式模板让 C++ 编译器生成您想要的代码。您可能希望在此处利用模板库——Boost spirit有一个不错的实现起点,即使您没有编写解析器,也可以重用它。

Windows Workflow Foundation还解决了如何编写顺序样式代码但将其作为短的运行到完成片段运行的问题。他们求助于在 XML 中指定控制流。显然没有直接重用标准 C# 语法的方法。他们仅将其作为数据结构(a-la JSON)提供。如果愿意的话,在 Qt 中实现 XML 和基于代码的 WF 会很简单。尽管 .NET 和 C# 为代码的程序化生成提供了充足的支持,但所有这一切……

于 2012-07-11T13:31:45.330 回答
2

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

通常你不应该继承QThread. 您这样做可能是因为您阅读了 Qt 文档,不幸的是它仍然建议子类QThread化 - 即使开发人员很久以前写了一篇博客文章指出您不应该子类化QThread。不幸的是,他们仍然没有适当地更新文档。

我建议阅读Qt 博客上的“你做错了”,然后使用“Kari”的答案作为如何设置基本多线程系统的示例。

于 2012-07-11T11:10:41.297 回答
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()执行冗长操作的时间。

于 2012-07-11T06:38:57.520 回答