Files
2026-07-22 18:08:10 +08:00

6.2 KiB
Raw Permalink Blame History

我的建议是:

不要靠主线程固定定时器轮询 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

一句话:

计算可以用定时器调度,刷新不要用定时器轮询;刷新应该由后台完成事件驱动,再由主线程事件循环合并执行。