Asynchronously Wait for Task to Complete with Timeout

with 3 Comments

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?

Take One – Task Extension Method

The plan was to implement som kind of extension method to Task which could add the timeout-functionality.

And use it like this.

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 DoStuffInnerAsync and TimeoutAfter, 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.

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 to ct.Register(...) might actually make my application crash if ct was canceled outside of the TimeOutAfter-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.

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.

Easy peasy. I wish I had known about this earlier!

Follow Johan Classon:

Azure enthusiast, .Net Developer, PowerShell empowered DevOps hacker, and Solutions Architect.

3 Responses

  1. Andreas
    | Reply


    Thank you for sharing your journey to the obvious. I saw the method and searched for some solution to extend a task to wrap the timeout in a extension method. It seems you can just put the cancellation token in the constructor.

    But one thing I am wondering is what happens if your task finishes before the timeout. Will it stop the cancellation token source?

    The CancellationTokenSource is implementing the IDisposable interface. So maybe it would be stopped if you dispose it after your await or use the using construct.



    • Johan Classon
      | Reply

      Late answer, but here is one anyway… You can reuse CTS in many tasks, although once cancelation is requested you need to create a new one. Disposing CTS might be a good idea if you use it with CancelAfter or CreateLinkedTokenSource.

  2. Matt
    | Reply

    I think your final solution is rather different to the others.

    Any client awaiting on your earlier extension method solutions will continue execution after, at most, the timeout period..

    Your final solution gives no such guarantees; if the Task does not immediately honour the token cancellation (e.g. it’s blocking on a response somewhere, not regularly checking for token cancellation, or just off in la-la land), the client doing the await may still hang forever.

    Unless I’m missing something?

Leave a Reply