With Rx, how do I ignore all-except-the-latest value when my Subscribe method is running

后端 未结 9 1544
暖寄归人
暖寄归人 2020-11-29 03:47

Using Reactive Extensions, I want to ignore messages coming from my event stream that occur while my Subscribe method is running. I.e. it sometimes takes me lon

相关标签:
9条回答
  • 2020-11-29 03:48

    Here is a method that is similar to Dave's but uses Sample instead (which is more appropriate than buffer). I've included a similar extension method to the one I added to Dave's answer.

    The extension:

    public static IDisposable SubscribeWithoutOverlap<T>(this IObservable<T> source, Action<T> action)
    {
        var sampler = new Subject<Unit>();
    
        var sub = source.
            Sample(sampler).
            ObserveOn(Scheduler.ThreadPool).
            Subscribe(l =>
            {
                action(l);
                sampler.OnNext(Unit.Default);
            });
    
        // start sampling when we have a first value
        source.Take(1).Subscribe(_ => sampler.OnNext(Unit.Default));
    
        return sub;
    }
    

    Note that it's simpler, and there is no 'empty' buffer that's fired. The first element that is sent to the action actually comes from the stream itself.

    Usage is straightforward:

    messages.SubscribeWithoutOverlap(n =>
    {
        Console.WriteLine("start: " + n);
        Thread.Sleep(500);
        Console.WriteLine("end: " + n);
    });
    
    messages.Subscribe(x => Console.WriteLine("source: " + x)); // for testing
    

    And results:

    source: 0
    start: 0
    source: 1
    source: 2
    source: 3
    source: 4
    source: 5
    end: 0
    start: 5
    source: 6
    source: 7
    source: 8
    source: 9
    source: 10
    end: 5
    start: 10
    source: 11
    source: 12
    source: 13
    source: 14
    source: 15
    end: 10
    
    0 讨论(0)
  • 2020-11-29 03:50

    An example using Observable.Switch. It also handles the case when you complete the task but there is nothing in the queue.

    using System.Reactive.Linq;
    using System.Reactive.Subjects;
    using System.Reactive.Concurrency;
    using System.Reactive.Disposables;
    
    namespace System.Reactive
    {
        public static class RXX
        {
            public static IDisposable SubscribeWithoutOverlap<T>
            ( this IObservable<T> source
            , Action<T> action
            , IScheduler scheduler = null)
            {
                var sampler = new Subject<Unit>();
                scheduler = scheduler ?? Scheduler.Default;
                var p = source.Publish();
                var connection = p.Connect();
    
                var subscription = sampler.Select(x=>p.Take(1))
                    .Switch()
                    .ObserveOn(scheduler)
                    .Subscribe(l =>
                    {
                        action(l);
                        sampler.OnNext(Unit.Default);
                    });
    
                sampler.OnNext(Unit.Default);
    
                return new CompositeDisposable(connection, subscription);
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-29 03:56

    Just finished (and already completely revised) my own solution to the problem, which I plan to use in production.

    Unless the scheduler uses the current thread, calls to OnNext, OnCompleted, OnError from the source should return immediately; if the observer is busy with previous notifications, they go into a queue with a specifiable maximum size, from where they'll be notified whenever the previous notification has been processed. If the queue fills up, least recent items are discarded. So, a maximum queue size of 0 ignores all items coming in while the observer is busy; a size of 1 will always let observe the latest item; a size up to int.MaxValue keeps the consumer busy until it catches up with the producer.

    If the scheduler supports long running (ie gives you a thread of your own), I schedule a loop to notify the observer; otherwise I use recursive scheduling.

    Here's the code. Any comments are appreciated.

    partial class MoreObservables
    {
        /// <summary>
        /// Avoids backpressure by enqueuing items when the <paramref name="source"/> produces them more rapidly than the observer can process.
        /// </summary>
        /// <param name="source">The source sequence.</param>
        /// <param name="maxQueueSize">Maximum queue size. If the queue gets full, less recent items are discarded from the queue.</param>
        /// <param name="scheduler">Optional, default: <see cref="Scheduler.Default"/>: <see cref="IScheduler"/> on which to observe notifications.</param>
        /// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="maxQueueSize"/> is negative.</exception>
        /// <remarks>
        /// A <paramref name="maxQueueSize"/> of 0 observes items only if the subscriber is ready.
        /// A <paramref name="maxQueueSize"/> of 1 guarantees to observe the last item in the sequence, if any.
        /// To observe the whole source sequence, specify <see cref="int.MaxValue"/>.
        /// </remarks>
        public static IObservable<TSource> Latest<TSource>(this IObservable<TSource> source, int maxQueueSize, IScheduler scheduler = null)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (maxQueueSize < 0) throw new ArgumentOutOfRangeException(nameof(maxQueueSize));
            if (scheduler == null) scheduler = Scheduler.Default;
    
            return Observable.Create<TSource>(observer => LatestImpl<TSource>.Subscribe(source, maxQueueSize, scheduler, observer));
        }
    
        private static class LatestImpl<TSource>
        {
            public static IDisposable Subscribe(IObservable<TSource> source, int maxQueueSize, IScheduler scheduler, IObserver<TSource> observer)
            {
                if (observer == null) throw new ArgumentNullException(nameof(observer));
    
                var longrunningScheduler = scheduler.AsLongRunning();
                if (longrunningScheduler != null)
                    return new LoopSubscription(source, maxQueueSize, longrunningScheduler, observer);
    
                return new RecursiveSubscription(source, maxQueueSize, scheduler, observer);
            }
    
            #region Subscriptions
    
            /// <summary>
            /// Represents a subscription to <see cref="Latest{TSource}(IObservable{TSource}, int, IScheduler)"/> which notifies in a loop.
            /// </summary>
            private sealed class LoopSubscription : IDisposable
            {
                private enum State
                {
                    Idle, // nothing to notify
                    Head, // next notification is in _head
                    Queue, // next notifications are in _queue, followed by _completion
                    Disposed, // disposed
                }
    
                private readonly SingleAssignmentDisposable _subscription = new SingleAssignmentDisposable();
                private readonly IObserver<TSource> _observer;
                private State _state;
                private TSource _head; // item in front of the queue
                private IQueue _queue; // queued items
                private Notification<TSource> _completion; // completion notification
    
                public LoopSubscription(IObservable<TSource> source, int maxQueueSize, ISchedulerLongRunning scheduler, IObserver<TSource> observer)
                {
                    _observer = observer;
                    _queue = Queue.Create(maxQueueSize);
                    scheduler.ScheduleLongRunning(_ => Loop());
                    _subscription.Disposable = source.Subscribe(
                        OnNext,
                        error => OnCompletion(Notification.CreateOnError<TSource>(error)),
                        () => OnCompletion(Notification.CreateOnCompleted<TSource>()));
                }
    
                private void OnNext(TSource value)
                {
                    lock (_subscription)
                    {
                        switch (_state)
                        {
                            case State.Idle:
                                _head = value;
                                _state = State.Head;
                                Monitor.Pulse(_subscription);
                                break;
                            case State.Head:
                            case State.Queue:
                                if (_completion != null) return;
                                try { _queue.Enqueue(value); }
                                catch (Exception error) // probably OutOfMemoryException
                                {
                                    _completion = Notification.CreateOnError<TSource>(error);
                                    _subscription.Dispose();
                                }
                                break;
                        }
                    }
                }
    
                private void OnCompletion(Notification<TSource> completion)
                {
                    lock (_subscription)
                    {
                        switch (_state)
                        {
                            case State.Idle:
                                _completion = completion;
                                _state = State.Queue;
                                Monitor.Pulse(_subscription);
                                _subscription.Dispose();
                                break;
                            case State.Head:
                            case State.Queue:
                                if (_completion != null) return;
                                _completion = completion;
                                _subscription.Dispose();
                                break;
                        }
                    }
                }
    
                public void Dispose()
                {
                    lock (_subscription)
                    {
                        if (_state == State.Disposed) return;
    
                        _head = default(TSource);
                        _queue = null;
                        _completion = null;
                        _state = State.Disposed;
                        Monitor.Pulse(_subscription);
                        _subscription.Dispose();
                    }
                }
    
                private void Loop()
                {
                    try
                    {
                        while (true) // overall loop for all notifications
                        {
                            // next notification to emit
                            Notification<TSource> completion;
                            TSource next; // iff completion == null
    
                            lock (_subscription)
                            {
                                while (true)
                                {
                                    while (_state == State.Idle)
                                        Monitor.Wait(_subscription);
    
                                    if (_state == State.Head)
                                    {
                                        completion = null;
                                        next = _head;
                                        _head = default(TSource);
                                        _state = State.Queue;
                                        break;
                                    }
                                    if (_state == State.Queue)
                                    {
                                        if (!_queue.IsEmpty)
                                        {
                                            completion = null;
                                            next = _queue.Dequeue(); // assumption: this never throws
                                            break;
                                        }
                                        if (_completion != null)
                                        {
                                            completion = _completion;
                                            next = default(TSource);
                                            break;
                                        }
                                        _state = State.Idle;
                                        continue;
                                    }
                                    Debug.Assert(_state == State.Disposed);
                                    return;
                                }
                            }
    
                            if (completion != null)
                            {
                                completion.Accept(_observer);
                                return;
                            }
                            _observer.OnNext(next);
                        }
                    }
                    finally { Dispose(); }
                }
            }
    
            /// <summary>
            /// Represents a subscription to <see cref="Latest{TSource}(IObservable{TSource}, int, IScheduler)"/> which notifies recursively.
            /// </summary>
            private sealed class RecursiveSubscription : IDisposable
            {
                private enum State
                {
                    Idle, // nothing to notify
                    Scheduled, // emitter scheduled or executing
                    Disposed, // disposed
                }
    
                private readonly SingleAssignmentDisposable _subscription = new SingleAssignmentDisposable();
                private readonly MultipleAssignmentDisposable _emitter = new MultipleAssignmentDisposable(); // scheduled emit action
                private readonly IScheduler _scheduler;
                private readonly IObserver<TSource> _observer;
                private State _state;
                private IQueue _queue; // queued items
                private Notification<TSource> _completion; // completion notification
    
                public RecursiveSubscription(IObservable<TSource> source, int maxQueueSize, IScheduler scheduler, IObserver<TSource> observer)
                {
                    _scheduler = scheduler;
                    _observer = observer;
                    _queue = Queue.Create(maxQueueSize);
                    _subscription.Disposable = source.Subscribe(
                        OnNext,
                        error => OnCompletion(Notification.CreateOnError<TSource>(error)),
                        () => OnCompletion(Notification.CreateOnCompleted<TSource>()));
                }
    
                private void OnNext(TSource value)
                {
                    lock (_subscription)
                    {
                        switch (_state)
                        {
                            case State.Idle:
                                _emitter.Disposable = _scheduler.Schedule(value, EmitNext);
                                _state = State.Scheduled;
                                break;
                            case State.Scheduled:
                                if (_completion != null) return;
                                try { _queue.Enqueue(value); }
                                catch (Exception error) // probably OutOfMemoryException
                                {
                                    _completion = Notification.CreateOnError<TSource>(error);
                                    _subscription.Dispose();
                                }
                                break;
                        }
                    }
                }
    
                private void OnCompletion(Notification<TSource> completion)
                {
                    lock (_subscription)
                    {
                        switch (_state)
                        {
                            case State.Idle:
                                _completion = completion;
                                _emitter.Disposable = _scheduler.Schedule(() => EmitCompletion(completion));
                                _state = State.Scheduled;
                                _subscription.Dispose();
                                break;
                            case State.Scheduled:
                                if (_completion != null) return;
                                _completion = completion;
                                _subscription.Dispose();
                                break;
                        }
                    }
                }
    
                public void Dispose()
                {
                    lock (_subscription)
                    {
                        if (_state == State.Disposed) return;
    
                        _emitter.Dispose();
                        _queue = null;
                        _completion = null;
                        _state = State.Disposed;
                        _subscription.Dispose();
                    }
                }
    
                private void EmitNext(TSource value, Action<TSource> self)
                {
                    try { _observer.OnNext(value); }
                    catch { Dispose(); return; }
    
                    lock (_subscription)
                    {
                        if (_state == State.Disposed) return;
                        Debug.Assert(_state == State.Scheduled);
                        if (!_queue.IsEmpty)
                            self(_queue.Dequeue());
                        else if (_completion != null)
                            _emitter.Disposable = _scheduler.Schedule(() => EmitCompletion(_completion));
                        else
                            _state = State.Idle;
                    }
                }
    
                private void EmitCompletion(Notification<TSource> completion)
                {
                    try { completion.Accept(_observer); }
                    finally { Dispose(); }
                }
            }
    
            #endregion
    
            #region IQueue
    
            /// <summary>
            /// FIFO queue that discards least recent items if size limit is reached.
            /// </summary>
            private interface IQueue
            {
                bool IsEmpty { get; }
                void Enqueue(TSource item);
                TSource Dequeue();
            }
    
            /// <summary>
            /// <see cref="IQueue"/> implementations.
            /// </summary>
            private static class Queue
            {
                public static IQueue Create(int maxSize)
                {
                    switch (maxSize)
                    {
                        case 0: return Zero.Instance;
                        case 1: return new One();
                        default: return new Many(maxSize);
                    }
                }
    
                private sealed class Zero : IQueue
                {
                    // ReSharper disable once StaticMemberInGenericType
                    public static Zero Instance { get; } = new Zero();
                    private Zero() { }
    
                    public bool IsEmpty => true;
                    public void Enqueue(TSource item) { }
                    public TSource Dequeue() { throw new InvalidOperationException(); }
                }
    
                private sealed class One : IQueue
                {
                    private TSource _item;
    
                    public bool IsEmpty { get; private set; } = true;
    
                    public void Enqueue(TSource item)
                    {
                        _item = item;
                        IsEmpty = false;
                    }
    
                    public TSource Dequeue()
                    {
                        if (IsEmpty) throw new InvalidOperationException();
    
                        var item = _item;
                        _item = default(TSource);
                        IsEmpty = true;
                        return item;
                    }
                }
    
                private sealed class Many : IQueue
                {
                    private readonly int _maxSize, _initialSize;
                    private int _deq, _enq; // indices of deque and enqueu positions
                    private TSource[] _buffer;
    
                    public Many(int maxSize)
                    {
                        if (maxSize < 2) throw new ArgumentOutOfRangeException(nameof(maxSize));
    
                        _maxSize = maxSize;
                        if (maxSize == int.MaxValue)
                            _initialSize = 4;
                        else
                        {
                            // choose an initial size that won't get us too close to maxSize when doubling
                            _initialSize = maxSize;
                            while (_initialSize >= 7)
                                _initialSize = (_initialSize + 1) / 2;
                        }
                    }
    
                    public bool IsEmpty { get; private set; } = true;
    
                    public void Enqueue(TSource item)
                    {
                        if (IsEmpty)
                        {
                            if (_buffer == null) _buffer = new TSource[_initialSize];
                            _buffer[0] = item;
                            _deq = 0;
                            _enq = 1;
                            IsEmpty = false;
                            return;
                        }
                        if (_deq == _enq) // full
                        {
                            if (_buffer.Length == _maxSize) // overwrite least recent
                            {
                                _buffer[_enq] = item;
                                if (++_enq == _buffer.Length) _enq = 0;
                                _deq = _enq;
                                return;
                            }
    
                            // increse buffer size
                            var newSize = _buffer.Length >= _maxSize / 2 ? _maxSize : 2 * _buffer.Length;
                            var newBuffer = new TSource[newSize];
                            var count = _buffer.Length - _deq;
                            Array.Copy(_buffer, _deq, newBuffer, 0, count);
                            Array.Copy(_buffer, 0, newBuffer, count, _deq);
                            _deq = 0;
                            _enq = _buffer.Length;
                            _buffer = newBuffer;
                        }
                        _buffer[_enq] = item;
                        if (++_enq == _buffer.Length) _enq = 0;
                    }
    
                    public TSource Dequeue()
                    {
                        if (IsEmpty) throw new InvalidOperationException();
    
                        var result = ReadAndClear(ref _buffer[_deq]);
                        if (++_deq == _buffer.Length) _deq = 0;
                        if (_deq == _enq)
                        {
                            IsEmpty = true;
                            if (_buffer.Length > _initialSize) _buffer = null;
                        }
                        return result;
                    }
    
                    private static TSource ReadAndClear(ref TSource item)
                    {
                        var result = item;
                        item = default(TSource);
                        return result;
                    }
                }
            }
    
            #endregion
        }
    }
    
    0 讨论(0)
  • 2020-11-29 03:57

    Yet another solution.

    This is not pretty, because it mixes Task and Observable, so it's not really testable using ReactiveTest (though to be honest, I'm not sure how I'd implement a 'slow' subscriber with ReactiveTest either).

    public static IObservable<T> ShedLoad<T>(this IObservable<T> source)
    {
        return Observable.Create<T>(observer =>
        {
            Task task = Task.FromResult(0);
            return source.Subscribe(t =>
            {
                if(task.IsCompleted)
                    task = Task.Run(() => observer.OnNext(t));
                else
                    Debug.WriteLine("Skip, task not finished");
            }, observer.OnError, observer.OnCompleted);
        });
    }
    

    I'm guessing there might be a race condition in there, but to my mind, if we're at the stage where we're ditching stuff because it's going too fast, I don't mind ditching one too many or too few. Oh, and each OnNext is called (potentially) on a different thread (I guess I could put a Synchronize on the back of the Create).

    I admit I couldn't get the Materialize extension to work properly (I hooked it up to a FromEventPattern(MouseMove) and then subscribed with a deliberately slow Subscribe, and weirdly it would let bursts of events through, rather than one at at time)

    0 讨论(0)
  • 2020-11-29 03:58

    I've written a blog post about this with a solution that uses CAS instead of locks and avoids recursion. The code is below, but you can find a complete explanation here: http://www.zerobugbuild.com/?p=192

    public static IObservable<TSource> ObserveLatestOn<TSource>(
        this IObservable<TSource> source,
        IScheduler scheduler)
    {
        return Observable.Create<TSource>(observer =>
        {
            Notification<TSource> pendingNotification = null;
            var cancelable = new MultipleAssignmentDisposable();
    
            var sourceSubscription = source.Materialize()
                .Subscribe(notification =>
                {
                    var previousNotification = Interlocked.Exchange(
                        ref pendingNotification, notification);
    
                    if (previousNotification != null) return;
    
                    cancelable.Disposable = scheduler.Schedule(() =>
                        {
                            var notificationToSend = Interlocked.Exchange(
                                ref pendingNotification, null);
                            notificationToSend.Accept(observer);
                        });
                });
                return new CompositeDisposable(sourceSubscription, cancelable);
        });
    }
    
    0 讨论(0)
  • 2020-11-29 03:59

    Here is an attempt using "just" Rx. The timer and the subscriber are kept independent by observing on the threadpool and I've used a subject to provide feedback on completing the task.

    I don't think this is a simple solution, but I hope it might give you ideas for improvement.

    messages.
        Buffer(() => feedback).
        Select(l => l.LastOrDefault()).
        ObserveOn(Scheduler.ThreadPool).
        Subscribe(n =>
        {
            Thread.Sleep(TimeSpan.FromMilliseconds(250));
            Console.WriteLine(n);
            feedback.OnNext(Unit.Default);
        });
    
    feedback.OnNext(Unit.Default);
    

    There is one slight problem -- the buffer is first closed when it's empty so it generates the default value. You could probably solve it by doing the feedback after the first message.


    Here it is as an extension function:

    public static IDisposable SubscribeWithoutOverlap<T>(this IObservable<T> source, Action<T> action)
    {
        var feedback = new Subject<Unit>();
    
        var sub = source.
            Buffer(() => feedback).
            ObserveOn(Scheduler.ThreadPool).
            Subscribe(l =>
            {
                action(l.LastOrDefault());
                feedback.OnNext(Unit.Default);
            });
    
        feedback.OnNext(Unit.Default);
    
        return sub;
    }
    

    And usage:

        messages.SubscribeWithoutOverlap(n =>
        {
            Thread.Sleep(1000);
            Console.WriteLine(n);
        });
    
    0 讨论(0)
提交回复
热议问题