【问题标题】:Observable.Interval not updating UI with expected frequencyObservable.Interval 未以预期频率更新 UI
【发布时间】:2017-11-27 13:08:16
【问题描述】:

我正在尝试模拟时钟的行为。为此,我创建了一个Observable.Interval,它在指定的时间间隔后,将属性的值更新相同的时间量。该值是数据绑定到 GUI 的。

问题是:“时钟”的运行速度比预期的慢很多,也就是说,时钟值增加一秒需要超过一秒的时间。

所需的时间分辨率越精细(下面的 MILLIS_INTERVAL 的值),问题就越严重(我用 1、10、100 和 1000 的其他约数对其进行了测试,因为 TimeSpan.FromMilliseconds 被记录为具有最大分辨率为 1 毫秒)。

我希望 observable 回调异步运行,并定期触发,可能并行触发,尽管执行需要时间,但似乎频率越大,滞后越多。

视图模型

使用系统; 使用 System.ComponentModel; 使用 System.Reactive.Linq; 命名空间间隔时钟 { 公共类 ViewModel:INotifyPropertyChanged { 公共双 TimeSeconds { 得到 { 返回 _timeSeconds; } 放 { _timeSeconds = 值; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("TimeSeconds")); } } 双 _timeSeconds; 常量双 MILLIS_INTERVAL = 10; 公共视图模型() { Observable.Interval(TimeSpan.FromMilliseconds(MILLIS_INTERVAL)) .Subscribe(token => TimeSeconds += MILLIS_INTERVAL / 1000); } 公共事件 PropertyChangedEventHandler PropertyChanged; } }

主窗口:

<Window x:Class="IntervalClock.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:IntervalClock"
        mc:Ignorable="d">

    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>

    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBlock Text="{Binding TimeSeconds, StringFormat=N3}" FontSize="30"/>
    </StackPanel>
</Window>

【问题讨论】:

  • @mm8 我该如何解决这个问题?
  • 你的意思是让回调并行运行,彼此独立?
  • Windows 不是实时操作系统,因此计时器并不完美。你可能会有明显的漂移。此外,Windows 上的最小分辨率为 15.6 毫秒 - 因此,无论您做什么,任何小于此值的有效时间都是 15.6 毫秒。
  • @mm8 是的。我只是尝试将回调放在 Task.Run() 中,但无济于事。
  • @mm8 - 计时器在同一个线程上调用OnNext

标签: c# wpf mvvm system.reactive


【解决方案1】:

Observable.Interval 运算符计算运行每个 .OnNext 以触发的间隔。因此,如果.OnNext 需要 0.1 秒并且您的间隔为 1.0 秒,那么您的有效期限为 1.1 秒。但当然,Windows 不是实时操作系统,所以它有漂移。您的实际时间可能会有更大的变化 - 尤其是当您的系统处于负载状态时。

我会这样做:

var interval = TimeSpan.FromSeconds(1.0);
Observable
    .Create<long>(o =>
    {
        var sw = Stopwatch.StartNew();
        return
            Observable
                .Timer(TimeSpan.Zero, TimeSpan.FromSeconds(interval.TotalSeconds / 10.0))
                .Select(x => (long)sw.Elapsed.TotalSeconds)
                .DistinctUntilChanged()
                .Subscribe(o);
    })

使用StopWatch 可为您提供出色的时间准确度。计时器的触发频率大约是 interval 的 10 倍,因此它在触发时会出现 +/- 10% 的错误,但结果是相同的 - 您从这个可观察到的值返回了一个非常准确的“秒”值,只是不是每秒都精确返回。

【讨论】:

  • 非常感谢您的回答。我可能给人的印象是我想制作一种时钟,但我真正想要的是“时钟”播放图像序列,这些图像序列是从相机以已知帧速率拍摄的。因此,对于 30 的帧速率,问题表述将是“我需要一个可以在每 1/30 秒内异步触发回调的 observable”。您的解决方案将消除累积漂移的问题,这是我的主要问题,但在我看来,帧到帧会有点抖动,可能会跳过一些帧。
  • 另一种可能性,也是一种非常好的可能性(虽然可能有点违反 MVVM),是订阅 View 中的 CompositionTarget.Rendering 事件(显示图像序列的实际 UserControl),这样时钟信号将来自渲染引擎。然后它可以从秒表中获取经过的时间,并计算出相应的帧索引来显示。
【解决方案2】:

计时器似乎在同一个线程上调用OnNext,因此回调没有“异步”执行。

您可以创建自己的方法来调用线程池线程上的回调。下面是一个使用System.Timers.Timer 类的基本示例:

public static IObservable<long> Interval(TimeSpan interval)
{
    return Observable.Create<long>(observer =>
    {
        long i = 0;
        Timer _timer = new Timer(interval.TotalMilliseconds);
        _timer.Elapsed += (s, e) => observer.OnNext(i++);
        _timer.Start();

        return Disposable.Create(() =>
        {
            _timer.Stop();
            _timer.Dispose();
        });
    });
}

用法(只需调用您自己的Interval 方法而不是Observable.Interval):

Interval(TimeSpan.FromMilliseconds(MILLIS_INTERVAL))
                  .Subscribe(token => TimeSeconds += MILLIS_INTERVAL / 1000);

这应该使“回调异步运行,并定期触发,可能是并行的,尽管执行需要时间......”。

【讨论】:

  • 既然不是我,我需要问:为什么要投反对票?
  • @heltonbiker - Observable.Interval 在同一线程上调用 OnNext。第一句话就是错的。此外,还有一个 Rx 操作员已经按照 mm8 的建议进行操作 - Observable.Timer(interval)。无需创建此代码。此代码也不会使其定期触发 - Windows 中的所有计时器都会漂移。
  • @heltonbiker - 运营商是Observable.Timer(interval, interval)。您必须重复间隔,否则计时器不会重复。
猜你喜欢
  • 2011-02-25
  • 2020-09-15
  • 2021-11-22
  • 2018-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多