Observable.Interval not updating UI with expected

2019-09-21 01:32发布

I am trying to simulate the behavior of a clock. For that, I create an Observable.Interval that, after a specified time interval, updates the value of a property by the equivalent amount of time. The value is data-bound to the GUI.

The problem is: the "clock" runs quite slower than expected, that is, it takes longer than one second for the clock value to increase one second.

The finer the desired time resolution (the value of MILLIS_INTERVAL below), the worse the problem gets (I tested it with values of 1, 10, 100 and other submultiples of 1000, since TimeSpan.FromMilliseconds is documented to have a maximum resolution of 1ms).

I would expect the observable callback to run asynchronously, and be fired at regular intervals, possibly in parallel, despite the time it takes to execute, but it seems that the larger the frequency, the more it lags.

ViewModel

using System;
using System.ComponentModel;
using System.Reactive.Linq;

namespace IntervalClock
{
    public class ViewModel : INotifyPropertyChanged
    {

        public double TimeSeconds
        {
            get { return _timeSeconds; }
            set
            {
                _timeSeconds = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("TimeSeconds"));
            }
        }
        double _timeSeconds;


        const double MILLIS_INTERVAL = 10;

        public ViewModel()
        {
            Observable.Interval(TimeSpan.FromMilliseconds(MILLIS_INTERVAL))
                      .Subscribe(token => TimeSeconds += MILLIS_INTERVAL / 1000);
        }


        public event PropertyChangedEventHandler PropertyChanged;
    }
}

MainWindow:

<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>

2条回答
男人必须洒脱
2楼-- · 2019-09-21 01:57

The timer seems to call OnNext on the same thread so the callback isn't being executed "asynchronously".

You could create your own method that invokes the callback on a thread pool thread. Here is a basic example using the System.Timers.Timer class:

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();
        });
    });
}

Usage (just call your own Interval method instead of Observable.Interval):

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

This should make the "callback run asynchronously, and be fired at regular intervals, possibly in parallel, despite the time it takes to execute, ...".

查看更多
相关推荐>>
3楼-- · 2019-09-21 02:19

The Observable.Interval operator times the interval between running each .OnNext to fire. So if the .OnNext take 0.1s and your interval is 1.0s then you have an effective period of 1.1s. But of course Windows isn't a real-time operating system so it has drift. Your actual time can vary even more - especially when your system is under load.

Here's how I would do this:

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);
    })

The use of the StopWatch gives you excellent accuracy for the time elapsed. The timer fires roughly 10x more often than the interval so that it has a +/- 10% error on when it fires, but the result is the same - you have a very accurate "seconds" value returned from this observable, just not return precisely on every second.

查看更多
登录 后发表回答