我尝试使用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.
从输出中可以看出,行"作业终止"不是最后一行,这意味着在应用程序终止之前,清理工作没有足够的时间来完成。
答案 0 :(得分:2)
如果我正确理解了这个问题,这不是一个Rx问题,这是一个“无论你是在做什么,你在订阅'问题。您的订阅操作需要半秒钟,清理可能需要另外半秒,并且您的作业终止需要几秒钟。您希望在取消和终止之间挤压什么?
我能给你的最好建议是让订阅操作更好地兑现取消令牌,而不是Thread.Sleep
来电。
答案 1 :(得分:2)
使用similar question的答案和a question about waiting before terminating的答案,我找到了一个能够达到我想要的解决方案。
我最初的问题是我发现无法等待订阅的主题。上面链接的答案让我以三种方式重构代码:
我将取消逻辑从订阅转移到了observable。
订阅包含在自己的Task
中(因此执行可以继续到ReadLine
- 语句)。
引入ManualResetEvent
来控制应用程序退出策略。
解决方案:
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来说,我不知道这是否是解决我问题的最佳解决方案。但这是对问题中发布的示例的一个很大的改进,因为它符合我的要求:
ManualResetEvent
发出信号)。 TakeWhile
- 方法中的生产者(而不是消费者)。Finally
- 方法中流取消的反应。这是一个更好的解决方案。
答案 2 :(得分:1)
Observables 是 (a)waitable。对 observable 的订阅是不可等待的。因此,如果您想等待订阅代码完成,而不求助于使用 ManualResetEvent
等人工解决方案,您应该使订阅代码成为派生 observable 的副作用,并且 (a) 等待该 observable。您的问题中提供的示例有额外的要求,这使问题有点复杂,但没有那么多:
您想在订阅 observable 和等待它完成之间做其他事情(Console.ReadLine()
等)。
您希望在取消 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.");
说明:
.TakeUntil...cts.Token.Register...
是一种惯用的方式,可以在 Interval
取消时立即取消订阅 cts.Token
observable。它是从 relevant question 复制粘贴的。您也可以使用更简单的 .TakeWhile(x => !cts.Token.IsCancellationRequested)
,前提是您可以取消响应性稍差的取消。
Do
运算符是执行订阅副作用的自然方式,因为它与 Subscribe
方法具有相同的参数。
.Publish().AutoConnect(0);
立即使序列变热。 AutoConnect
运算符无法断开与底层 observable 的连接(与 RefCount
运算符相反),但在这种特殊情况下,不需要断开连接功能。底层 observable 的生命周期已经由我们之前附加的 CancellationToken
控制。
.DefaultIfEmpty()
之前的 .Wait()
是必需的,以防止在生成任何元素之前取消序列的边缘情况下的 InvalidOperationException
。如果您 await
异步序列,它也是必需的。这些等待 observable 的机制(以及其他类似 RunAsync
和 ToTask
运算符)正在返回 observable 发出的最后一个值,如果没有这样的值存在,它们就会变得沮丧。