简体   繁体   中英

Create a cancellable IObservable from an Action

I want to create a utility method that creates an IObservable for an Action which is only called at subscription time AND! which follows a SubscribeOn(...) directive. Here is my implementation, which is based from what I could extract from http://www.introtorx.com and other resources, but it fails in one specific case:

    /// <summary>
    /// Makes an observable out of an action. Only at subscription the task will be executed. 
    /// </summary>
    /// <param name="action">The action.</param>
    /// <returns></returns>
    public static IObservable<Unit> MakeObservable_2(Action action)
    {
        return Observable.Create<Unit>(
            observer =>
            {
                return System.Reactive.Concurrency.CurrentThreadScheduler.Instance.Schedule(
                    () =>
                    {
                        try
                        {
                            action();
                            observer.OnNext(Unit.Default);
                            observer.OnCompleted();
                        }
                        catch (Exception ex)
                        {
                            observer.OnError(ex);
                        }
                    });
            });
    }

I hoped that the usage of the CurrrentThreadScheduler would lead to the usage of the Scheduler given in SubscribeOn(). This implementation works for .SubscribeOn(TaskPoolScheduler.Default) but not for .SubscribeOn(Dispatcher.CurrentDispatcher). Could you please change the above implementation such that all unit test below pass?

    [Test]
    public void RxActionUtilities_MakeObservableFromAction_WorksAsExpected()
    {
        ManualResetEvent evt = new ManualResetEvent(false);

        // Timeout of this test if sth. goes wrong below
        Task.Factory.StartNew(() =>
        {
            Thread.Sleep(5000);
            Console.WriteLine("Test timed out!");
            evt.Set();
        });

        int threadIdOfAction = -42;
        int threadIdOfSubscriptionContect = -43;
        bool subscriptionWasCalled = false;

        Action action = () =>
            {
                threadIdOfAction = Thread.CurrentThread.ManagedThreadId;
                Console.WriteLine("This is an action on thread " + threadIdOfAction);
            };

        var observable = RxActionUtilities.MakeObservable_2(action);

        threadIdOfSubscriptionContect = Thread.CurrentThread.ManagedThreadId;
        Console.WriteLine("Before subscription on thread " + threadIdOfSubscriptionContect);

        // The next line is the one I want to have working, but the subscription is never executed
        observable.SubscribeOn(Dispatcher.CurrentDispatcher).Subscribe(
            //observable.Subscribe( // would pass
            (unit) =>
            {
                Console.WriteLine("Subscription: OnNext " + threadIdOfAction + ", " + threadIdOfSubscriptionContect);
                subscriptionWasCalled = true;
            },
            (ex) => evt.Set(), () => evt.Set());

        Console.WriteLine("After subscription");

        evt.WaitOne();

        Assert.AreNotEqual(-42, threadIdOfAction);
        Assert.AreNotEqual(-43, threadIdOfSubscriptionContect);

        Assert.AreEqual(threadIdOfAction, threadIdOfSubscriptionContect);
        Assert.That(subscriptionWasCalled);
    }

    [Test]
    // This test passes with the current implementation
    public void RxActionUtilities_MakeObservableFromActionSubscribeOnDifferentThread_WorksAsExpected()
    {
        ManualResetEvent evt = new ManualResetEvent(false);

        // Timeout of this test if sth. goes wrong below
        Task.Factory.StartNew(() =>
        {
            Thread.Sleep(5000);
            Console.WriteLine("Test timed out!");
            evt.Set();
        });

        int threadIdOfAction = 42;
        int threadIdOfSubscriptionContect = 43;
        bool subscriptionWasCalled = false;

        Action action = () =>
        {
            threadIdOfAction = Thread.CurrentThread.ManagedThreadId;
            Console.WriteLine("This is an action on thread " + threadIdOfAction);
        };

        var observable = RxActionUtilities.MakeObservable_2(action);

        threadIdOfSubscriptionContect = Thread.CurrentThread.ManagedThreadId;
        Console.WriteLine("Before subscription on thread " + threadIdOfSubscriptionContect);

        // The next line is the one I want to have working, but the subscription is never executed
        observable.SubscribeOn(TaskPoolScheduler.Default).Subscribe(
            (unit) =>
            {
                Console.WriteLine("Subscription: OnNext " + threadIdOfAction + ", " + threadIdOfSubscriptionContect);
                subscriptionWasCalled = true;
            },
            (ex) => evt.Set(), () => evt.Set());

        evt.WaitOne();

        Console.WriteLine("After subscription");

        Assert.AreNotEqual(-42, threadIdOfAction);
        Assert.AreNotEqual(-43, threadIdOfSubscriptionContect);
        Assert.AreNotEqual(threadIdOfAction, threadIdOfSubscriptionContect);
        Assert.That(subscriptionWasCalled);
    }


    [Test]
    public void RxActionUtilities_MakeObservableFromAction_IsCancellable()
    {
        ManualResetEvent evt = new ManualResetEvent(false);

        // Timeout of this test if sth. goes wrong below
        Task.Factory.StartNew(() =>
        {
            Thread.Sleep(5000);
            Console.WriteLine("Test timed out!");
            evt.Set();
        });

        int threadIdOfAction = -42;
        int threadIdOfSubscriptionContect = -43;
        bool subscriptionWasCalled = false;
        bool actionTerminated = false;

        Action action = () =>
        {
            threadIdOfAction = Thread.CurrentThread.ManagedThreadId;

            for (int i = 0; i < 10; ++i)
            {
                Console.WriteLine("Some action #" + i);

                Thread.Sleep(200);
            }

            actionTerminated = true;
            evt.Set();
        };

        var observable = RxActionUtilities.MakeObservable_2(action);

        threadIdOfSubscriptionContect = Thread.CurrentThread.ManagedThreadId;
        Console.WriteLine("Before subscription on thread " + threadIdOfSubscriptionContect);

        var subscription =
            observable.SubscribeOn(TaskPoolScheduler.Default).Subscribe(
                (unit) =>
                {
                    Console.WriteLine("Subscription: OnNext " + threadIdOfAction + ", " + threadIdOfSubscriptionContect);
                    subscriptionWasCalled = true;
                },
                (ex) => evt.Set(), () => evt.Set());

        Console.WriteLine("After subscription");

        Thread.Sleep(1000);
        Console.WriteLine("Killing subscription ...");
        subscription.Dispose();
        Console.WriteLine("... done.");

        evt.WaitOne();

        Assert.IsFalse(actionTerminated);

        Assert.AreNotEqual(-42, threadIdOfAction);
        Assert.AreNotEqual(-43, threadIdOfSubscriptionContect);

        Assert.AreEqual(threadIdOfAction, threadIdOfSubscriptionContect);
        Assert.That(subscriptionWasCalled);
    }

Update

In reaction to Lee's elaborate answers I give it another try and reformulate my question. IIUC we can summarize that

  • You cannot stop an action that has already started
  • I completely misunderstood Dispatcher.CurrentDispatcher and how it works: AFAICS it should never be used as argument to SubscribeOn(), but rather only as argument to ObserveOn.
  • I misunderstood CurrentThreadScheduler

In order to create something that is cancellable we need an action that is aware of cancellation, eg by using an Action<CancellationToken> . Here is my next try. Please tell me whether you think that this implementation fits well into the Rx framework or whether we can improve this again:

public static IObservable<Unit> 
    MakeObservable(Action<CancellationToken> action, IScheduler scheduler)
{
    return Observable.Create<Unit>(
        observer
        =>
        {
            // internally creates a new CancellationTokenSource
            var cancel = new CancellationDisposable(); 

            var scheduledAction = scheduler.Schedule(() =>
            {
                try
                {
                    action(cancel.Token);
                    observer.OnCompleted();
                }
                catch (Exception ex)
                {
                    observer.OnError(ex);
                }
            });

            // Cancellation before execution of action is performed 
            // by disposing scheduledAction
            // Cancellation during execution of action is performed 
            // by disposing cancel
            return new CompositeDisposable(cancel, scheduledAction);
        });
}

And if you are at it: I could not figure out how to test this using TestScheduler s:

[Test]
public void MakeObservableFromCancelableAction_CancellationTakesPlaceWithTrueThread()
{
    var scheduler = NewThreadScheduler.Default;

    Action<CancellationToken> action =
        (cancellationToken) =>
        {
            for (int i = 0; i < 10; ++i)
            {
                Console.WriteLine("Some action #" + i);

                if (cancellationToken.IsCancellationRequested)
                {
                    break;
                }

                Thread.Sleep(20);
                // Hoping that the disposal of the subscription stops 
                // the loop before we reach i == 4.
                Assert.Less(i, 4);
            }
        };

    var observable = RxActionUtilities.MakeObservable(action, scheduler);

    var subscription = observable.Subscribe((unit) => { });

    Thread.Sleep(60);

    subscription.Dispose();
}

I think you can make you code a lot simpler, you can also make you tests far simpler. The beauty with Rx is that you should be able to do away with all the Task/Thread/ManualResetEvent. Also I assume that you could also just use NUnit's [Timeout] attribute instead of your custom code.

Anyway... @Per is right, Observable.Start is what you are looking for. You pass it an Action and an IScheduler, which seems exactly what you want.

[Test]
public void Run_Action_as_IOb_on_scheduler_with_ObStart()
{
    var scheduler = new TestScheduler();
    var flag = false;
    Action action = () => { flag = true; };

    var subscription = Observable.Start(action, scheduler)
                                    .Subscribe();

    Assert.IsFalse(flag);
    scheduler.AdvanceBy(1);
    Assert.IsTrue(flag);
    subscription.Dispose(); //Not required as the sequence will have completed and then auto-detached.
}

However you may notice that it does have some odd behavior (in V1 that I have on this PC at least). Specifically, Observable.Start will just run the Action immediately, and not actually wait for the observable sequence to be subscribed to. Also due to this, calling subscribe, then disposing of the subscription before the action should have been executed has not effect. Hmmmmm.

[Test]
public void Run_Action_as_IOb_on_scheduler_with_ObStart_dispose()
{
    var scheduler = new TestScheduler();
    var flag = false;
    Action action = () => { flag = true; };

    var subscription = Observable.Start(action, scheduler).Subscribe();


    Assert.IsFalse(flag);
    subscription.Dispose();
    scheduler.AdvanceBy(1);
    Assert.IsFalse(flag);   //FAILS. Oh no! this is true!
}
[Test]
public void Run_Action_as_IOb_on_scheduler_with_ObStart_no_subscribe()
{
    var scheduler = new TestScheduler();
    var flag = false;
    Action action = () => { flag = true; };

    Observable.Start(action, scheduler);
    //Note the lack of subscribe?!

    Assert.IsFalse(flag);
    scheduler.AdvanceBy(1);
    Assert.IsFalse(flag);//FAILS. Oh no! this is true!
}

However we can follow your path of using Observable.Create. You are so close, however, you just don't need to do any scheduling in the Create delegate. Just trust Rx to do this for you.

[Test]
public void Run_Action_as_IOb_on_scheduler_with_ObCreate()
{
    var scheduler = new TestScheduler();
    var flag = false;
    Action action = () => { flag = true; };

    var subscription = Observable.Create<Unit>(observer =>
        {
            try
            {
                action();
                observer.OnNext(Unit.Default);
                observer.OnCompleted();
            }
            catch (Exception ex)
            {
                observer.OnError(ex);
            }
            return Disposable.Empty;
        })
        .SubscribeOn(scheduler)
        .Subscribe();   //Without subscribe, the action wont run.

    Assert.IsFalse(flag);
    scheduler.AdvanceBy(1);
    Assert.IsTrue(flag);
    subscription.Dispose(); //Not required as the sequence will have completed and then auto-detached.
}

[Test]
public void Run_Action_as_IOb_on_scheduler_with_ObCreate_dispose()
{
    var scheduler = new TestScheduler();
    var flag = false;
    Action action = () => { flag = true; };

    var subscription = Observable.Create<Unit>(observer =>
    {
        try
        {
            action();
            observer.OnNext(Unit.Default);
            observer.OnCompleted();
        }
        catch (Exception ex)
        {
            observer.OnError(ex);
        }
        return Disposable.Empty;
    })
        .SubscribeOn(scheduler)
        .Subscribe();   //Without subscribe, the action wont run.

    Assert.IsFalse(flag);
    subscription.Dispose();
    scheduler.AdvanceBy(1);
    Assert.IsFalse(flag);   //Subscription was disposed before the scheduler was able to run, so the action did not run.
}

If you hope to be able to cancel the actual action mid-way through the action being processed, then you will need to do some more advanced stuff than this.

Final implementation is simply:

public static class RxActionUtilities
{
    /// <summary>
    /// Makes an observable out of an action. Only at subscription the task will be executed. 
    /// </summary>
    /// <param name="action">The action.</param>
    /// <returns></returns>
    /// <example>
    /// <code>
    /// <![CDATA[
    /// RxActionUtilities.MakeObservable_3(myAction)
    ///                  .SubscribeOn(_schedulerProvider.TaskPoolScheduler)
    ///                  .Subscribe(....);
    /// 
    /// ]]>
    /// </code>
    /// </example>
    public static IObservable<Unit> MakeObservable_3(Action action)
    {
        return Observable.Create<Unit>(observer =>
            {
                try
                {
                    action();
                    observer.OnNext(Unit.Default);
                    observer.OnCompleted();
                }
                catch (Exception ex)
                {
                    observer.OnError(ex);
                }
                return Disposable.Empty;
            });
    }
}

I hope that helps.

EDIT: Wrt to you usage of the Dispatcher in your Unit tests. I think that first you should try to understand how that works before applying another layer (Rx) to add to the confusion. One of the key benefits that Rx brings to me when coding in WPF is the abstraction of the Dispatcher via a Scheduler. It allows me to test concurrency in WPF easily. For example, this simple test here fails:

[Test, Timeout(2000)]
public void DispatcherFail()
{
    var wasRun = false;
    Action MyAction = () =>
        {
            Console.WriteLine("Running...");
            wasRun = true;
            Console.WriteLine("Run.");
        };
    Dispatcher.CurrentDispatcher.BeginInvoke(MyAction);

    Assert.IsTrue(wasRun);
}

If you run this, you will notice that nothing is even printed to the console so we dont have a race condition, the action is just never run. The reason for this is that the dispatcher has not started it's message loop. To correct this test we have to fill it up with messy infrastructure code.

[Test, Timeout(2000)]
public void Testing_with_Dispatcher_BeginInvoke()
{
    var frame = new DispatcherFrame();  //1 - The Message loop
    var wasRun = false;
    Action MyAction = () =>
    {
        Console.WriteLine("Running...");
        wasRun = true;
        Console.WriteLine("Run.");
        frame.Continue = false;         //2 - Stop the message loop, else we hang forever
    };
    Dispatcher.CurrentDispatcher.BeginInvoke(MyAction);

    Dispatcher.PushFrame(frame);        //3 - Start the message loop

    Assert.IsTrue(wasRun);
}

So we clearly dont want to do this for all of our tests that need concurrency in WPF. It would be a nightmare trying to inject frame.Continue=false into actions we dont control. Luckily IScheudler exposes all that we need via it's Schedule methods.

Next CurrentThreadScheduler should be thought of as a Trampoline, not as a SynchronizationContext (which is what I think you think it is).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM