简体   繁体   中英

Observable.Create: CancellationToken doesn't transition to IsCancellationRequested

Take this little script (designed in LINQPad but should run everywhere):

void Main()
{
    Task.Run(() => Worker()).Wait();
}

async Task Worker()
{
    if (SynchronizationContext.Current != null)
        throw new InvalidOperationException("Don't want any synchronization!");

    BaseClass provider = new Implementation();
    Func<IObserver<TimeSpan>, CancellationToken, Task> subscribeAsync =
        provider.CreateValues;
    var observable = Observable.Create(subscribeAsync);

    var cancellation = new CancellationTokenSource(5500).Token; // gets cancelled after 5.5s
    cancellation.Register(() => Console.WriteLine("token is cancelled now"));
    await observable
        .Do(ts =>
        {
            Console.WriteLine("Elapsed: {0}; cancelled: {1}",
                ts,
                cancellation.IsCancellationRequested);
            cancellation.ThrowIfCancellationRequested();
        })
        .ToTask(cancellation)
        .ConfigureAwait(false);
}

abstract class BaseClass
{
    // allow implementers to use async-await
    public abstract Task CreateValues(IObserver<TimeSpan> observer, CancellationToken cancellation);
}

class Implementation : BaseClass
{
    // creates Values for 10s; entirely CPU-bound: no way for async-await hence return Task.CompletedTask
    public override Task CreateValues(IObserver<TimeSpan> observer, CancellationToken cancellation)
    {
        try
        {
            var sw = Stopwatch.StartNew();
            for (int i = 0; i < 10; i++)
            {
                for (int j = 0; j < 3; j++)
                {
                    Console.WriteLine("{0}/{1} cancelled:{2}", i, j, cancellation.IsCancellationRequested);
                    Thread.Sleep(333);
                }

                if (cancellation.IsCancellationRequested) // !! never gets true !!
                    throw new ApplicationException("token is cancelled");

                observer.OnNext(sw.Elapsed);
            }

            return Task.CompletedTask;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            throw;
        }
    }
}

The method Implementation.CreateValues justs keeps running for the entire 10 seconds instead of stopping after 5.5s. The CancellationToken passed in by Observable.Create doesn't even transition to a cancelled state (the original token of course does)!

Is it a bug? Is it my fault by doing something wrong?

Output is:

0/0 cancelled:False
0/1 cancelled:False
0/2 cancelled:False
Elapsed: 00:00:01.0205951; cancelled: False
1/0 cancelled:False
1/1 cancelled:False
1/2 cancelled:False
Elapsed: 00:00:02.0253279; cancelled: False
2/0 cancelled:False
2/1 cancelled:False
2/2 cancelled:False
Elapsed: 00:00:03.0274035; cancelled: False
3/0 cancelled:False
3/1 cancelled:False
3/2 cancelled:False
Elapsed: 00:00:04.0294796; cancelled: False
4/0 cancelled:False
4/1 cancelled:False
4/2 cancelled:False
Elapsed: 00:00:05.0315332; cancelled: False
5/0 cancelled:False
5/1 cancelled:False
token is cancelled now
5/2 cancelled:False
Elapsed: 00:00:06.0335601; cancelled: True
6/0 cancelled:False
6/1 cancelled:False
6/2 cancelled:False
Elapsed: 00:00:07.0436211; cancelled: True
7/0 cancelled:False
7/1 cancelled:False
7/2 cancelled:False
Elapsed: 00:00:08.0457921; cancelled: True
8/0 cancelled:False
8/1 cancelled:False
8/2 cancelled:False
Elapsed: 00:00:09.0477509; cancelled: True
9/0 cancelled:False
9/1 cancelled:False
9/2 cancelled:False
Elapsed: 00:00:10.0498751; cancelled: True
[AggregateException] at Main/Task.Wait()

The cancellation token getting passed to the subscribeAsync function is instantiated by the Observable.Create call and is not the cancellation token you're instantiating.

As per the Observable.Create overload summary:

Creates an observable sequence from a specified cancellable asynchronous Subscribe method. The CancellationToken passed to the asynchronous Subscribe method is tied to the returned disposable subscription, allowing best-effort cancellation.

In short, the cancellation token will get cancelled when you dispose of the subscription, not after the specified delay.

You should be able to refactor your code as follows to make it work:

Observable.Create(observer => subscribeAsync(observer, cancellation));

Hope it helps.

This is not really an answer to the question but a rewrite of the sample code using System.Threading.Tasks.Dataflow inplace of System.Reactive (far too much code for being posted as a comment):

This has several advantages:

  1. since the observer parameter is now a Task every implementation has something to await for.
  2. the processing code previously in Do() (now in ActionBlock ) can itself be implemented async if desired.
  3. integrated buffering if desired.
  4. it's decoupled = technology agnostic: My interface is Func<TimeSpan, Task<bool>> and so there is no dependency on Rx or TPL-Dataflow or what else.

New code:

void Main()
{
    Task.Run(() => Worker()).Wait();
    Console.WriteLine("DONE");
}

async Task Worker()
{
    if (SynchronizationContext.Current != null)
        throw new InvalidOperationException("Don't want any synchronization!");

    var cancellation = new CancellationTokenSource(55000).Token; // gets cancelled after 5.5s
    cancellation.Register(() => Console.WriteLine("token is cancelled now"));

    var flow = new ActionBlock<TimeSpan>(
        async ts =>
        {
            Console.WriteLine("[START] Elapsed: {0}; cancelled: {1}", ts, cancellation.IsCancellationRequested);
            await Task.Delay(2500).ConfigureAwait(false); // processing takes more time than items need to produce
            Console.WriteLine("[STOP] Elapsed: {0}; cancelled: {1}", ts, cancellation.IsCancellationRequested);
        },
        new ExecutionDataflowBlockOptions
        {
            BoundedCapacity = 2, // Buffer 1 item ahead
            EnsureOrdered = true,
            CancellationToken = cancellation,
        });

    Func<TimeSpan, Task<bool>> observer = ts => flow.SendAsync(ts, cancellation);

    BaseClass provider = new Implementation();
    await provider.CreateValues(observer, cancellation).ConfigureAwait(false);
    Console.WriteLine("provider.CreateValues done");

    flow.Complete();
    await flow.Completion.ConfigureAwait(false);
    Console.WriteLine("flow completed");
}

abstract class BaseClass
{
    // allow implementers to use async-await
    public abstract Task CreateValues(Func<TimeSpan, Task<bool>> observer, CancellationToken cancellation);
}

class Implementation : BaseClass
{
    public override async Task CreateValues(Func<TimeSpan, Task<bool>> observer, CancellationToken cancellation)
    {
        try
        {
            var sw = Stopwatch.StartNew();
            for (int i = 0; i < 10; i++)
            {
                for (int j = 0; j < 3; j++)
                {
                    Console.WriteLine("{0}/{1} cancelled:{2}", i, j, cancellation.IsCancellationRequested);
                    Thread.Sleep(333);
                }

                if (cancellation.IsCancellationRequested)
                    throw new ApplicationException("token is cancelled");

                var value = sw.Elapsed;
                var queued = await observer(value); // use of "observer" encorces async-await even if there is nothing else async
                Console.WriteLine("[{0}] '{1}' @ {2}", queued ? "enqueued" : "skipped", value, sw.Elapsed);

                if (!queued)
                    ; // Dispose item
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            throw;
        }
    }
}

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