Reputation: 1339
I'm a little confused about how to implement cancellation tokens for the following case.
Say I have a method that has a cancellation token with no timeout specified like this.
public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
try
{
Task.Delay(1000, cancellationToken)
}
catch (OperationCanceledException canceledException)
{
// Do something with canceledException
Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
throw;
}
catch (Exception exception)
{
// Do something with exception
Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
throw;
}
}
But in that method I want to call another method that expects a CancellationToken
except this time I want to set up a timeout on it, like this.
public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
try
{
var innerCancellationTokenSource = new CancellationTokenSource();
innerCancellationTokenSource.CancelAfter(1000);
var innerCancellationToken = innerCancellationTokenSource.Token;
await DoSomeElseAsyncThingAsync(innerCancellationToken);
}
catch (OperationCanceledException canceledException)
{
// Do something with canceledException
Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
throw;
}
catch (Exception exception)
{
// Do something with exception
Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
throw;
}
}
How do I get the innerCancellationToken
to respect the cancellation request from the cancellationToken
parameter?
The best I can come up with is something like this:
public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
try
{
await Task.WhenAny(
DoSomeElseAsyncThingAsync(cancellationToken),
KaboomAsync(100, cancellationToken)
);
}
catch (OperationCanceledException canceledException)
{
// Do something with canceledException
Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
throw;
}
catch (Exception exception)
{
// Do something with exception
Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
throw;
}
}
public static async Task KaboomAsync(int delay, CancellationToken cancellationToken = default)
{
await Task.Delay(delay, cancellationToken);
throw new OperationCanceledException();
}
But this isn't quite right; the KaboomAsync()
function will always blow up, and this path seems gnarly. Is there a better pattern for this?
Post answer I created this static Util methods to save me putting that boilerplate in a million times.
Hopefully its useful to someone.
public static async Task<T> CancellableUnitOfWorkHelper<T>(
Func<CancellationToken, Task<T>> unitOfWordFunc,
int timeOut,
CancellationToken cancellationToken = default
)
{
try
{
var innerCancellationTokenSource = new CancellationTokenSource(timeOut);
using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken))
return await unitOfWordFunc(linkedTokenSource.Token);
}
catch (OperationCanceledException canceledException)
{
Console.WriteLine(
cancellationToken.IsCancellationRequested
? "Manual or parent Timeout {0}"
: "UnitOfWork Timeout {0}"
, canceledException
);
throw;
}
catch (Exception exception)
{
Console.WriteLine("Exception {0}", exception);
throw;
}
}
public static async Task CancellableUnitOfWorkHelper(
Func<CancellationToken, Task> unitOfWordFunc,
int timeOut,
CancellationToken cancellationToken = default
)
{
try
{
var innerCancellationTokenSource = new CancellationTokenSource(timeOut);
using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken))
await unitOfWordFunc(linkedTokenSource.Token);
}
catch (OperationCanceledException canceledException)
{
Console.WriteLine(
cancellationToken.IsCancellationRequested
? "Manual or parent Timeout {0}"
: "UnitOfWork Timeout {0}"
, canceledException
);
throw;
}
catch (Exception exception)
{
Console.WriteLine("Exception {0}", exception);
throw;
}
}
They can be used like this.
await Util.CancellableUnitOfWorkHelper(
token => Task.Delay(1000, token),
200
);
or
await Util.CancellableUnitOfWorkHelper(
token => Task.Delay(1000, token),
200,
someExistingToken
);
In both examples it will timeout after 200 ms but the second will also respect manual cancels or timeouts from the "someExistingToken" token.
Upvotes: 2
Views: 3865
Reputation: 70671
CancellationTokenSource
has a method specifically for this scenario: CreateLinkedTokenSource
In your example, it might look something like this:
public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
try
{
var innerCancellationTokenSource = new CancellationTokenSource();
using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken))
{
innerCancellationTokenSource.CancelAfter(1000);
await DoSomeElseAsyncThingAsync(linkedTokenSource.Token);
}
}
catch (OperationCanceledException canceledException)
{
// Do something with canceledException
Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
throw;
}
catch (Exception exception)
{
// Do something with exception
Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
throw;
}
}
Note that it's important to dispose the linked source, otherwise references from the parent token sources will prevent it from being garbage-collected.
See also Any way to differentiate Cancel and Timeout and When to dispose CancellationTokenSource?
Upvotes: 9