6.2 KiB
6.2 KiB
我的建议是:
不要靠主线程固定定时器轮询 update。
后台生成完新图后,提交一个刷新任务到 Qt 主事件循环。
如果你要限帧,再用单次 QTimer 做合并/节流。
也就是主线程刷新应该是 事件驱动,不是一直定时扫。
1. 不推荐固定主线程 timer 轮询 update
比如这种方式:
主线程每 16ms 扫描所有图
如果 rendered_version > painted_version
update()
它能工作,但问题是:
没有新图时也一直醒
图很多时每次都要扫一遍
后台刚生成完图,最多要等一个 timer 周期
主线程刷新逻辑和后台完成事件脱节
如果你有很多图,这种轮询会变成一种隐性开销。
尤其是图不可见、数据静止时,timer 仍然在跑,这不优雅。
2. 更推荐:后台发布 ready 后投递主线程刷新任务
后台线程生成完像素图:
CPU 线程生成 working
↓
发布到 ready
↓
发现 rendered_version > requested_paint_version
↓
投递一个 Qt 主线程任务
↓
主线程里调用 update(region)
注意:后台线程不要直接调用 QWidget 的 update()。Qt GUI 对象应该在 GUI 线程使用。
应该用:
QMetaObject::invokeMethod(widget, [this] {
widget->update(dirty_region);
}, Qt::QueuedConnection);
或者用 signal/slot,连接方式是 Qt::QueuedConnection。
3. 但是要加 pending,避免疯狂投递
后台很多图同时完成时,不能每个图都投递一个主线程任务。
应该有一个全局 pending 标志:
std::atomic_bool update_pending = false;
请求刷新:
void Render_Scheduler::request_update(Chart_Node* chart) {
{
std::scoped_lock lock(dirty_lock);
dirty_region += chart->rect();
}
if (update_pending.exchange(true, std::memory_order_acq_rel)) {
return;
}
QMetaObject::invokeMethod(widget, [this] {
flush_update();
}, Qt::QueuedConnection);
}
主线程执行:
void Render_Scheduler::flush_update() {
QRegion region;
{
std::scoped_lock lock(dirty_lock);
region = dirty_region;
dirty_region = QRegion();
}
update_pending.store(false, std::memory_order_release);
if (!region.isEmpty()) {
widget->update(region);
}
}
这样效果是:
后台完成 100 个图
↓
只投递 1 个主线程刷新任务
↓
主线程合并 dirty region
↓
调用一次 update(region)
这个比固定 timer 更合适。
4. Qt 的 update 本身也会合并
update() 不是马上 paint,它只是请求 Qt 稍后重绘。
所以:
widget->update();
widget->update();
widget->update();
通常不会立刻触发三次 paintEvent(),Qt 会合并。
但是你自己这层仍然要合并,因为:
Qt 会合并 paint event
但你仍然可能投递了大量 QueuedConnection 任务
所以你自己的 update_pending 是为了减少主线程事件队列压力。
5. timer 应该用在哪里
timer 不是完全不用,而是用途要分清。
计算调度 timer:可以有
比如每 16ms / 33ms 扫描哪些图需要提交 CPU 任务:
Render_Scheduler::tick()
扫描 meta_version
提交必要 CPU 任务
这个 timer 管的是:
要不要开始后台计算
不是管:
要不要 update
也就是说:
计算调度可以定时
UI 刷新最好事件驱动
刷新节流 timer:可以用 single-shot
如果你想限制 UI 最多 60 FPS,可以不用立刻 invokeMethod -> update(),而是主线程启动一个单次 timer:
后台 ready
↓
投递主线程任务
↓
主线程发现 flush_timer 没启动
↓
启动 singleShot(16ms)
↓
timer 到了再 update(region)
这样是 节流 timer,不是一直轮询 timer。
6. 最推荐的刷新模型
我建议你最终用这个:
后台完成图像
↓
publish ready
↓
scheduler->request_update(chart)
↓
合并 dirty region
↓
如果没有 pending flush
invokeMethod 投递主线程 flush
↓
主线程 flush_update()
↓
widget->update(region)
↓
Qt 稍后调用 paintEvent
↓
paintEvent 中 ready -> front
↓
drawImage(front)
这个模型里:
没有新图,不投递刷新。
多个图完成,合并刷新。
主线程不轮询。
paintEvent 不计算。
7. 如果要限帧,用这个变体
void Render_Scheduler::request_update(Chart_Node* chart) {
{
std::scoped_lock lock(dirty_lock);
dirty_region += chart->rect();
}
QMetaObject::invokeMethod(widget, [this] {
if (flush_timer.isActive()) {
return;
}
flush_timer.start(16);
}, Qt::QueuedConnection);
}
timer timeout:
void Render_Scheduler::flush_update() {
QRegion region;
{
std::scoped_lock lock(dirty_lock);
region = dirty_region;
dirty_region = QRegion();
}
if (!region.isEmpty()) {
widget->update(region);
}
}
这个语义是:
后台可以随时完成图
但 UI 最多约 60Hz 请求 update
如果你想 30 FPS:
flush_timer.start(33)
如果你想尽快刷新:
invokeMethod 之后直接 update(region)
8. 最终结论
你的主线程不需要启动一个固定 timer 去不停判断:
rendered_version 是否变化
更合理的是:
后台生成完新 ready 后,投递一个刷新任务到 Qt 主事件循环。
然后根据需求加节流:
低延迟:QueuedConnection -> update(region)
限帧:QueuedConnection -> singleShot timer -> update(region)
所以最终分工是:
后台线程:
生成图
发布 ready
请求调度器刷新
调度器:
合并 dirty region
合并 queued update 请求
可选限帧
主线程:
执行 update(region)
在 paintEvent 中消费 ready 并贴 front
一句话:
计算可以用定时器调度,刷新不要用定时器轮询;刷新应该由后台完成事件驱动,再由主线程事件循环合并执行。