【问题标题】:Process lots of small tasks and keep the UI responsive处理大量小任务并保持 UI 响应
【发布时间】:2023-03-07 15:18:02
【问题描述】:

我有一个 WPF 应用程序,它需要对许多小任务进行一些处理。 这些小任务都是同时生成的,并以 Normal 的优先级添加到 Dispatcher Queue 中。同时显示忙碌指示符。结果是,尽管工作被分解为任务,但忙碌指示器实际上冻结了。

我尝试将这些任务的优先级更改为后台,看看是否解决了问题,但忙碌指示器仍然冻结。

我订阅了Dispatcher.Hooks.OperationStarted 事件,以查看在我的任务处理期间是否发生了任何渲染作业,但它们没有。

有什么想法吗?

一些技术细节: 这些任务实际上只是来自Observable 序列的消息,它们通过调用ReactiveUI 的ObserveOn(RxApp.MainThreadScheduler) 被“排队”到调度程序中,这应该等同于ObserveOn(DispatcherScheduler)。每个任务的工作部分是通过 ObserveOn 调用订阅的代码,例如

IObservable<TaskMessage> incomingTasks;
incomingTasks.ObserveOn(RxApp.MainThreadScheduler).Subscribe(SomeMethodWhichDoesWork);

在这个例子中,incomingTasks 可能会连续产生 3000 多条消息,ObserveOn 将每个对 SomeMethodWhichDoesWork 的调用推送到 Dispatcher 队列中,以便稍后处理

【问题讨论】:

    标签: wpf system.reactive dispatcher reactiveui


    【解决方案1】:

    基本问题

    您看到繁忙指示器停止的原因是您的SomeMethodWhichDoesWork 花费的时间太长。在它运行时,它会阻止 Dispatcher 上发生任何其他工作。

    为处理动画而生成的输入和渲染操作的优先级低于正常,但高于后台操作。但是,Dispatcher 上的操作不会被更高优先级操作的入队中断。因此,即使是后台操作,渲染操作也必须等待正在运行的操作。

    关于观察 DispatcherScheduler 的警告

    ObserveOn(DispatcherScheduler) 默认以正常优先级推送所有内容。较新版本的 Rx 具有允许您指定优先级的重载。

    需要强调的一点是经常被忽略的一点是,一旦项目不是一个接一个地到达,DispatcherScheduler 就会将它们排队到 Dispatcher。

    因此,如果您的 3000 个项目都非常接近,您将在 Dispatcher 上备份 3000 个正常优先级的操作,阻止所有相同或更低优先级的操作,直到它们完成 - 包括渲染操作。这几乎可以肯定是您所看到的 - 这意味着即使您在后台线程上完成了 UI 更新以外的所有工作,您仍可能会看到问题,具体取决于您的 UI 更新量。

    除此之外,您应该检查您没有在 UI 线程上运行整个订阅 - 正如 Lee 所说。我通常编写我的代码,以便我在后台线程上Subscribe,而不是使用 SubscribeOn,虽然这也很好。

    建议

    无论您做什么,都尽可能多地在后台线程上完成工作。这一点在 StackOverflow 和其他地方已经被彻底完成了。以下是一些很好的资源:

    如果您想让 UI 在面对大量小更新时保持响应,您可以:

    • 以较低的优先级安排项目,这既方便又方便 - 但如果您需要特定的优先级,则不太好
    • 将更新存储在您自己的队列中并将它们排入队列并让您运行的每个操作在最后一步调用队列中的下一个项目。

    大局

    值得退后一步,放眼大局。

    如果你分别将 3000 个项目连续转储到 UI 中,那将为用户做什么?充其量他们将运行刷新率为 100Hz 的显示器,可能更低。我发现每秒 10 帧速率对于大多数用途来说已经绰绰有余了。

    不仅如此,据说人类一次不能处理超过 5-9 位的信息 - 因此,您可能会找到比一次更新这么多信息更好的方法来聚合和显示信息。例如,利用主视图/详细视图而不是一次在屏幕上显示所有内容等。

    另一个选项是查看您的 UI 更新造成的工作量。一些控件(我在看你 XamDataGrid)可能有非常冗长的测量/排列布局操作。你能简化你的动画吗?使用更简单的可视化树?想想看起来像圆点的流行忙碌的微调器 - 但实际上它只是在改变它们的颜色。一个很好的效果,实现起来相当便宜。值得对您的应用程序进行概要分析,以了解时间的去向。

    我也会考虑从前到后的整体方法。如果您有理由确定要一次更新这么多项目,为什么不缓冲它们并分块管理它们呢?这可能会一直追溯到源头——这可能在某处的服务器上?在任何情况下,Rx 都有一些不错的运算符,例如 Buffer,它可以将单个项目的流转换为更大的列表 - 它具有可以按时间缓冲的重载大小。

    【讨论】:

    • 我已经有一段时间没有研究这个问题了,但即使改变优先级似乎也无济于事。我希望的是,我的 3000 个低优先级项目将与一些更高优先级的渲染项目交错运行。不幸的是,无论我尝试什么优先级,似乎我的 3000 个项目总是首先被处理,并且没有其他项目被交错:/
    • 我对 WPF 也有这个假设,但它似乎并没有那样工作。我没有进行更深入的调查,而是认为 WPF 调度程序非常宝贵,无法在其上执行任何可以在另一个线程上执行的操作。 Rx 对此非常有用。我无法使用 ReactiveUI,因为我不使用它,但标准的 WPF+Rx 效果很好。
    【解决方案2】:

    您是否尝试过使用.SubscribeOn(TaskPoolScheduler.TaskPool) 订阅不同的线程?

    【讨论】:

      【解决方案3】:

      @Pedro Pombeiro 有正确的答案。 您在 UI 上看到冻结的原因是您在 Dispatcher 上排队工作。这意味着工作将在 UI 线程上完成。您可以将 Dispatcher 视为一个消息泵,它不断从其每个队列中排出消息(您可以将其视为每个优先级 [SystemIdle、ApplicationIdle、ContextIdle、Background、Input、Loaded、Render、DataBind、Normal、Send ])

      将您的工作放到不同的优先级队列中,不会使其同时运行,只是异步运行。

      要使用 Rx 在另一个线程上运行您的工作,然后如上所述使用 SubscribeOn。请记住,然后使用 ObserveOn 将 UI 的任何更新安排回 Dispatcher。

      【讨论】:

      • 我仍然认为应该可以使用优先级来完成这个单线程。不过我并没有成功,而是回到了多线程解决方案。
      猜你喜欢
      • 1970-01-01
      • 2011-05-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-06-03
      相关资源
      最近更新 更多