22

Given a cancellation token, I'd like to create an awaitable task out of it, which is never complete but can be cancelled. I need it for a pattern like this, which IMO should be quite common:

async Task DoStuff(Task t, CancellationToken ct)
{
   // t was made from TaskCompletionSource, 
   // both t and ct are beyond my control

   Task t2 = TaskFromCancellationToken(ct);
   await Task.WhenAny(t, t2);

   // do stuff
}

The best idea I've got so far is this:

Task TaskFromCancelationToken(CancellationToken ct)
{
    return Task.Delay(Timeout.Infinite, ct);
}

Is there a better way to make this logic happen?

noseratio
  • 59,932
  • 34
  • 208
  • 486

2 Answers2

26

It's not extremely common, but it's common enough to be part of my AsyncEx library. I use something like this:

public static Task AsTask(this CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<object>();
    cancellationToken.Register(() => tcs.TrySetCanceled(),
        useSynchronizationContext: false);
    return tcs.Task;
}

Update: These days, I recommend using something like CancellationTokenTaskSource, which properly handles all lifetimes with no possibility of resource leaks.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 2
    But [`cancellationToken.Register`](https://msdn.microsoft.com/en-us/library/dd321635(v=vs.110).aspx) returns [`CancellationTokenRegistration`](https://msdn.microsoft.com/en-us/library/system.threading.cancellationtokenregistration(v=vs.110).aspx), which you never dispose of (it implements `IDisposable`). Isn't that a problem? – Paya Apr 08 '15 at 20:31
  • 1
    @Paya: Yes. The current version of AsyncEx introduces a [`ToCancellationTokenTaskSource` method](https://github.com/StephenCleary/AsyncEx/blob/master/Source/Nito.AsyncEx%20(NET45%2C%20Win8%2C%20WP8%2C%20WPA81)/CancellationTokenExtensions.cs#L28) which is recommended instead of `AsTask`. The [next version removes `AsTask` completely](https://github.com/StephenCleary/AsyncEx.Tasks/blob/master/src/Nito.AsyncEx.Tasks/CancellationTokenTaskSource.cs). – Stephen Cleary Apr 08 '15 at 20:47
  • Is there any reason to prefer that over the suggested `Task.Delay`? That one seems like a very elegant solution and does not come with the `IDisposable` "annoyance". – Paya Apr 08 '15 at 21:13
  • 1
    @Paya: You still have the same resource leak; it's just not as noticeable. – Stephen Cleary Apr 08 '15 at 21:28
  • Right. I guess there is no escaping `IDisposable` then. For the OP, maybe [UntilCompletionOrCancellation wrapper](https://msdn.microsoft.com/en-us/library/hh873173(v=vs.110).aspx#code-snippet-25) might be suitable alternative, if there is no need to keep the cancellation token task around. – Paya Apr 08 '15 at 22:05
  • @Paya: That's the same approach that I take in the new version of AsyncEx, with [`WaitAsync` and `WhenAnyAsync` extension methods](https://github.com/StephenCleary/AsyncEx.Tasks/blob/master/src/Nito.AsyncEx.Tasks/TaskExtensions.cs). – Stephen Cleary Apr 08 '15 at 23:47
  • 3
    @Paya in order not to leak the registration, all you have to do is dispose the `CancellationTokenSource` and it will dispose all its token's registrations. – torvin Apr 19 '15 at 08:24
  • Shouldn't you pass the cancellation token into `TrySetCanceled`? – StriplingWarrior Jun 18 '18 at 20:59
  • 2
    @StriplingWarrior: Yes. At the time I wrote this answer, that overload didn't exist. The [modern AsyncEx code](https://github.com/StephenCleary/AsyncEx/blob/edb2c6b66d41471008a56e4098f9670b5143617e/src/Nito.AsyncEx.Tasks/CancellationTokenTaskSource.cs#L29) does pass the CT. – Stephen Cleary Jun 18 '18 at 22:04
  • @StephenCleary, could you please mention and briefly describe your [`CancellationTokenTaskSource`](https://github.com/StephenCleary/AsyncEx/blob/master/src/Nito.AsyncEx.Tasks/CancellationTokenTaskSource.cs) in the answer so that people were better informed? – roxton Jul 11 '21 at 18:00
12
Task.Delay(Timeout.Infinite, cancellationToken)

The answer you suggest in your question is the best solution to my knowledge. Here's why:

  • It is safe
  • Very small piece of code required
  • Standard library

The Task.Delay approach is heavily used by a lot of folks as per my knowledge, and is also recommended on Microsoft blogs. MSDN Example.

Why write code (including tests) yourself leveraging TaskCompletionSource for converting a cancellation token to a task? It is preferable to leverage the standard libraries instead of reinventing the wheel; they are more likely to be bug free than your code.

user3613932
  • 1,219
  • 15
  • 16
  • 3
    Be warned that this call will always raise an `OperationCanceledException` when done, see docs here: https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.delay – Gian Marco Oct 23 '18 at 11:31
  • Note that the timeout is `Timeout.Infinite`. Therefore, effectively, in practice, `OperationCanceledException` should never be called. – user3613932 Apr 22 '20 at 20:22
  • 2
    `OperationCanceledException` will be raised when the cancellationToken transition to canceled state – Gian Marco Apr 23 '20 at 08:40
  • You are absolutely right. I somehow misread the `cancellationToken` as `CancellationToken.None`. – user3613932 Apr 24 '20 at 20:49
  • I don't think this is safe, it's safe only when the `cancellationToken` is canceled but what if it's never cancelled? you will have a never-end delay task. Each time using this will spawn such a never-end task. I think that would cause some issue if used too much. – Hopeless Jun 08 '21 at 02:24
  • If the cancellation token is never cancelled then the task is ongoing by definition. However, if you feel more comfortable, you can provide an upper limit in terms of time to `Task.Delay` in addition to the cancellation token. I have tried other approaches of creating a task using cancellation token too but this approach is the easiest and least error-prone, and hence, I recommend it. – user3613932 Jun 11 '21 at 19:55