可观察的订阅如何优雅地终止?

时间:2017-07-19 14:37:10

标签: c# system.reactive cancellation

我尝试使用Reactive Extensions(Rx)来处理数据流。但是,每个元素的处理可能需要一些时间。为了打破处理,我使用CancellationToken,这有效地停止了订阅。

当请求取消时,如何正常完成当前工作并正确终止而不丢失任何数据?

示例:

var cts = new CancellationTokenSource();
cts.Token.Register(() => Console.WriteLine("Token cancelled."));

var observable = Observable
    .Interval(TimeSpan.FromMilliseconds(250));

observable
    .Subscribe(
        value =>
            {
                Console.WriteLine(value);
                Thread.Sleep(500);

                if (cts.Token.IsCancellationRequested)
                {
                    Console.WriteLine("Cancellation detected on {0}.", value);
                    Thread.Sleep(500);
                    Console.WriteLine("Cleaning up done for {0}.", value);
                }
            },
        () => Console.WriteLine("Completed"),
        cts.Token);

Console.ReadLine();
cts.Cancel();
Console.WriteLine("Job terminated.");

输出:

0
1
2
Token cancelled.
Job terminated.
Cancellation detected on 2.
Cleaning up done for 2.

从输出中可以看出,行"作业终止"不是最后一行,这意味着在应用程序终止之前,清理工作没有足够的时间来完成。

3 个答案:

答案 0 :(得分:2)

如果我正确理解了这个问题,这不是一个Rx问题,这是一个“无论你是在做什么,你在订阅'问题。您的订阅操作需要半秒钟,清理可能需要另外半秒,并且您的作业终止需要几秒钟。您希望在取消和终止之间挤压什么?

我能给你的最好建议是让订阅操作更好地兑现取消令牌,而不是Thread.Sleep来电。

答案 1 :(得分:2)

使用similar question的答案和a question about waiting before terminating的答案,我找到了一个能够达到我想要的解决方案。

我最初的问题是我发现无法等待订阅的主题。上面链接的答案让我以三种方式重构代码:

  1. 我将取消逻辑从订阅转移到了observable。

  2. 订阅包含在自己的Task中(因此执行可以继续到ReadLine - 语句)。

  3. 引入ManualResetEvent来控制应用程序退出策略。

  4. 解决方案:

    var reset = new ManualResetEvent(false);
    
    var cts = new CancellationTokenSource();
    cts.Token.Register(() => Console.WriteLine("Token cancelled."));
    
    var observable = Observable
        .Interval(TimeSpan.FromMilliseconds(250))
        .TakeWhile(x => !cts.Token.IsCancellationRequested)
        .Finally(
            () =>
                {
                    Console.WriteLine("Finally: Beginning finalization.");
                    Thread.Sleep(500);
                    Console.WriteLine("Finally: Done with finalization.");
                    reset.Set();
                });
    
    await Task.Factory.StartNew(
        () => observable
            .Subscribe(
                value =>
                    {
                        Console.WriteLine("Begin: {0}", value);
                        Thread.Sleep(2000);
                        Console.WriteLine("End: {0}", value);
                    },
                () => Console.WriteLine("Completed: Subscription completed.")),
        TaskCreationOptions.LongRunning);
    
    Console.ReadLine();
    cts.Cancel();
    reset.WaitOne();
    Console.WriteLine("Job terminated.");
    

    输出:

    Begin: 0
    End: 0
    Begin: 1
    Token cancelled.
    End: 1
    Completed: Subscription completed.
    Finally: Beginning finalization.
    Finally: Done with finalization.
    Job terminated.
    

    对于Reactive Extensions来说,我不知道这是否是解决我问题的最佳解决方案。但这是对问题中发布的示例的一个很大的改进,因为它符合我的要求:

    • 允许每个OnNext操作运行完成。
    • 应用程序等待流处理完成(由ManualResetEvent发出信号)。
    • 将流取消逻辑移至TakeWhile - 方法中的生产者(而不是消费者)。
    • 应用程序终止逻辑是对生产者Finally - 方法中流取消的反应。

    这是一个更好的解决方案。

答案 2 :(得分:1)

Observables 是 (a)waitable。对 observable 的订阅是不可等待的。因此,如果您想等待订阅代码完成,而不求助于使用 ManualResetEvent 等人工解决方案,您应该使订阅代码成为派生 observable 的副作用,并且 (a) 等待该 observable。您的问题中提供的示例有额外的要求,这使问题有点复杂,但没有那么多:

  1. 您想在订阅 observable 和等待它完成之间做其他事情(Console.ReadLine() 等)。

  2. 您希望在取消 CancellationToken 时终止 observable。

以下是如何满足这些要求的示例。它仅展示了解决此问题的众多可用方法之一:

var cts = new CancellationTokenSource();
cts.Token.Register(() => Console.WriteLine("Token cancelled."));

var observable = Observable
    .Interval(TimeSpan.FromMilliseconds(250));

var withCancellation = observable
    .TakeUntil(Observable.Create<Unit>(observer =>
        cts.Token.Register(() => observer.OnNext(default))));

var withSideEffectsAndCancellation = withCancellation
    .Do(value =>
    {
        Console.WriteLine(value);
        Thread.Sleep(500);

        if (cts.Token.IsCancellationRequested)
        {
            Console.WriteLine("Cancellation detected on {0}.", value);
            Thread.Sleep(500);
            Console.WriteLine("Cleaning up done for {0}.", value);
        }
    }, () => Console.WriteLine("Completed"));

var hotWithSideEffectsAndCancellation = withSideEffectsAndCancellation
    .Publish()
    .AutoConnect(0);

Console.ReadLine();
cts.Cancel();

hotWithSideEffectsAndCancellation.DefaultIfEmpty().Wait();
// or await hotWithSideEffectsAndCancellation.DefaultIfEmpty();
Console.WriteLine("Job terminated.");

说明:

  1. .TakeUntil...cts.Token.Register... 是一种惯用的方式,可以在 Interval 取消时立即取消订阅 cts.Token observable。它是从 relevant question 复制粘贴的。您也可以使用更简单的 .TakeWhile(x => !cts.Token.IsCancellationRequested),前提是您可以取消响应性稍差的取消。

  2. Do 运算符是执行订阅副作用的自然方式,因为它与 Subscribe 方法具有相同的参数。

  3. .Publish().AutoConnect(0); 立即使序列变热。 AutoConnect 运算符无法断开与底层 observable 的连接(与 RefCount 运算符相反),但在这种特殊情况下,不需要断开连接功能。底层 observable 的生命周期已经由我们之前附加的 CancellationToken 控制。

  4. .DefaultIfEmpty() 之前的 .Wait() 是必需的,以防止在生成任何元素之前取消序列的边缘情况下的 InvalidOperationException。如果您 await 异步序列,它也是必需的。这些等待 observable 的机制(以及其他类似 RunAsyncToTask 运算符)正在返回 observable 发出的最后一个值,如果没有这样的值存在,它们就会变得沮丧。