Does the Rx library omits disposing of the CancellationTokenSources it creates?
Asked Answered
E

0

3

The Rx library includes operators that accept lambda parameters, and some of these lambdas are provided with a CancellationToken that is controlled by the library itself. Some examples of these operators are the FromAsync, StartAsync and Create:

// Converts an asynchronous action into an observable sequence. Each subscription
// to the resulting sequence causes the action to be started. The CancellationToken
// passed to the asynchronous action is tied to the observable sequence's subscription
// that triggered the action's invocation and can be used for best-effort cancellation.
public static IObservable<Unit> FromAsync(Func<CancellationToken, Task> actionAsync);

I was under the impression that the Rx library does a good job at managing the lifecycle of the CancellationTokenSources that obviously has to create behind the scenes, but I am not so sure any more. Let's first state that the documentation insists strongly that the CancellationTokenSources should be disposed of:

This type implements the IDisposable interface. When you have finished using an instance of the type, you should dispose of it either directly or indirectly. To dispose of the type directly, call its Dispose method in a try/catch block. To dispose of it indirectly, use a language construct such as using (in C#) or Using (in Visual Basic).

Also from here:

Always call Dispose before you release your last reference to the CancellationTokenSource. Otherwise, the resources it is using will not be freed until the garbage collector calls the CancellationTokenSource object's Finalize method.

I made the experiment below to test my assumptions. It uses reflection to read the private fields _source and _disposed of the types CancellationToken and CancellationTokenSource respectively (.NET 5).

CancellationToken capturedToken = default;
var subscription = Observable.FromAsync(async token =>
{
    capturedToken = token;
    token.Register(() => Console.WriteLine("Token canceled"));
    await Task.Delay(Timeout.Infinite, token);
})
.TakeUntil(Observable.Timer(TimeSpan.FromMilliseconds(500)))
.Finally(() => Console.WriteLine("The observable was terminated"))
.Subscribe();

Thread.Sleep(1000);

var cts = (CancellationTokenSource)(typeof(CancellationToken)
    .GetField("_source", BindingFlags.NonPublic | BindingFlags.Instance)
    .GetValue(capturedToken));
bool disposed = (bool)(typeof(CancellationTokenSource)
    .GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)
    .GetValue(cts));
Console.WriteLine($"IsCancellationRequested: {cts.IsCancellationRequested}");
Console.WriteLine($"IsDisposed: {disposed}");

Output:

Token canceled
The observable was terminated
IsCancellationRequested: True
IsDisposed: False

Try it on Fiddle (.NET Framework version, having differently named private fields)

The captured CancellationToken is inspected half a second after the asynchronous operation has been canceled and the observable has terminated. The _disposed field has the value false, indicating that the Dispose method of the associated CancellationTokenSource has not been invoked. Am I doing something wrong, or the Rx library indeed omits disposing of the CancellationTokenSources it creates?

.NET 5.0.1, System.Reactive 5.0.0, C# 9

Elevated answered 26/2, 2021 at 8:18 Comment(25)
The documentation over-states the situation: it says that about any type which implements IDisposable. In practice, CancellationTokenSource only needs disposing if you've used .CancelAfter(...) or CancellationToken.WaitHandle, and even then, the timer will clean itself up when it fires, and most people don't bother to dispose mutexes (as doing so often involves introducing a race). It's a bit like Task.Dispose: it's there in the API, but not something that's called most of the time in practiceFluter
@Fluter the issue is that the Task.Dispose documentation states explicitly that: "However, particularly if your app targets the .NET Framework 4.5 or later, there is no need to call Dispose unless performance or scalability testing indicates that, based on your usage patterns, your app's performance would be improved by disposing of tasks.". There is no such subtleties included in the docs for the CancellationTokenSource.Dispose method!Elevated
I said it's a "bit like" Task.Dispose. There are plenty of types where disposing, although part of the API, isn't generally necessary in practice, regardless of what the docs say. CTS, like mutexes, is just generally quite annoying to dispose correctly, and since disposing isn't actually required except in very rare cases, the general convention is not to bother.Fluter
@Fluter in that case it seems that the documentation for the CancellationTokenSource.Dispose method is wrong, and the docs should be updated with less strongly-worded language like "Always". Otherwise, are we supposed to stop trusting the documentation in general?Elevated
Always take the MSDN docs with a pinch of salt! They're good, and getting better, but not perfect.Fluter
Realistically though they probably won't change that wording, as everything they say becomes a contract that they can't break in future.Fluter
@Fluter could you find another disposable type in the whole .NET platform whose disposal is known to be unnecessary in practice, and the docs say that it should be always disposed before you release the last reference? If not, then it would seem that fixing the docs for this type should be a priority. The CancellationTokenSource is a type that becomes more and more relevant with each new API that is released, because of how important cooperative cancellation is (Thread.Abort is not even supported any more).Elevated
@Fluter "Realistically though they probably won't change that wording, as everything they say becomes a contract that they can't break in future." <== based on that we should probably take the documentation seriously, and dispose always our CancellationTokenSources. Otherwise our code may become broken in the future, if Microsoft decides to change something in the implementation.Elevated
MemoryStream is a classic doesn't-need-to-be-disposed. I already mentioned mutexes, not because they don't need to be, but because they're really hard to disposeFluter
All I'm doing is telling you how things are, and why experienced, knowledgeable people are writing code in the way that they are. Don't expect me to spend my time defending either MS or RxFluter
"could you find another disposable type in the whole .NET platform whose disposal is known to be unnecessary in practice" MemoryStream, HttpClient in all but a few cases, though there is a fair amount of them especially with base classesTouchdown
@Fluter should answer this, or self answerTouchdown
@Fluter so what would you think if the Rx library used Mutexes (or other WaitHandles) under the hood, and omitted disposing of them? Would you say that it's OK because it is difficult, and library authors are not expected to deal correctly with difficult things?Elevated
@00110001 the docs for the MemoryStream state that: "This type implements the IDisposable interface, but does not actually have any resources to dispose. This means that disposing it by directly calling Dispose() or by using a language construct such as using (in C#) or Using (in Visual Basic) is not necessary." So it's not a good counterexample IMHO.Elevated
Again, I'm not going to defend either Rx or MSDN. I'm just explaining the common wisdom on CancellationTokenSource. Stop challenging me.Fluter
Ok, yeah, I didn't realise it said that in the doc, well I could have sworn it never did... I think maybe you should make a github post, and post the link here and keep it updated, why not ?, it's an interesting questionTouchdown
"Difficult" means requiring extra code, extra time, and extra resources (such as additional locks), with a higher probability of bugs, for no gainFluter
@00110001 honestly I don't know who is to blame here. Either the docs are wrong and I should ask for them to be fixed, or the Rx library is buggy and I should ask for it to be fixed. And I can't say which is which, because I am not possessing the common wisdom that canton7 attributes to the experienced, knowledgeable people who are writing code the way they are. 😃Elevated
Perhaps nobody is to blame? Perhaps there's no blame which needs handing out? Perhaps engineering has grey areas with compromises which need to be made, based on knowledge and experience? I've shared the common wisdom, so now you possess it too!Fluter
@Fluter perhaps you should share this wisdom also with the poor confused guy I misinformed an hour ago, by telling them to trust the official documentation for the CancellationTokenSource.Dispose more than what people say in the internet...Elevated
Yesh, technically the doc isn't wrong, it's just stating worst case, which I guess is bettee than best case. I think it should have slightly more info thoughTouchdown
The thread that's linked there actually has a good amount of widsom in it: people are sharing experiences with problems disposing a CTS, someone quoted Stephen Toub (who's an authority on all things threading). There's a (sadly) deleted answer by Hans linking to a good blog post as well. I think they explain things better than I did tbhFluter
I am adding a direct link to Stephen Toub's quote, that @Fluter referred to.Elevated
I think this is worth raising an issue in the Rx .NET repo.Wardieu
@Wardieu yea, I could, but I am too lazy to do it honestly. :-)Elevated

© 2022 - 2024 — McMap. All rights reserved.