I was recently working at an async method which could possibly hang if some dependent stuff did not happen. To prevent the method from hanging, I wanted to implement some kind of timeout. Now, how can I make a task abort, given the following method signature?
1 | public class SomeClient |
Take One - Task Extension Method
The plan was to implement som kind of extension method to Task which could add the timeout-functionality.
1 | internal static class TaskExtensions |
And use it like this.
1 | public class SomeClient |
This design allowed that the internal Delay-task would be cancelled if the user of my API canceled the method call. Nice! But it also had some mayor disadvantages:
- No task will be canceled if either of them finish successfully, which leads to having tasks running in the background for no reason, eating system resources.
- I had to make sure to pass the same cancellation token both to
DoStuffInnerAsyncandTimeoutAfter, which might be something that could lead to mistakes further down.
Take Two - Expanding the Extension Method
To be able to cancel the TimeoutAfter-task i needed a CancellationTokenSource-instance, and pass its token to the TimeoutAfter-method. And I also wanted the TimeoutAfter-task to cancel if the user canceled the public API call.
This is what I came up with.
1 | internal static class TaskExtensions |
This is some seriously dangerous programming.
- By subscribing to cancel-events with
ct.Register(...)I opened upp the possibility for memory leaks if I do not unsubscribe somehow. - Also, using
cts(which can be disposed) in the delegate passed toct.Register(...)might actually make my application crash ifctwas canceled outside of theTimeOutAfter-method scope.
Register returns a disposable something, which when disposed will unsubscribe. By adding the inner using-block, I fixed both of these problems.
This made it possible to cancel the Delay-task when the actual task completed, but not the reverse. How should I solve the bigger problem, how to cancel the actual task if it would hang? Eating up system resources indefinitely…
Take Three - Changing the Public API
With much hesitation I finally decided to make a breaking change to the public API by replacing the CancellationToken with a CancellationTokenSource in the DoStuffAsync-method.
1 | public class SomeClient |
Nice! But this still did not solve that I had make sure to pass the same cts to both the actual task and the Delay-task.
Final Solution - Doing the Obvious
Most things is really easy when you know the answer. By accident I stumbled upon that CancellationTokenSource has a CancelAfter(...)-method. This solves my problem entirely without the need to update my public API.
1 | var client = new SomeClient(); |
Easy peasy. I wish I had known about this earlier!