问题在于您的线程代码产生了事件风暴。循环计数非常快——如此之快,以至于每 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# 为代码的程序化生成提供了充足的支持,但所有这一切……