Home > front end >  Transforming async lambda only throwing an OperationCanceledException to synchronous variant
Transforming async lambda only throwing an OperationCanceledException to synchronous variant

Time:01-13

I have this small test in .NET 6, which works fine (it's green).

Func<CancellationToken, Task> taskFactory = async token => throw new OperationCanceledException();
Assert.True(taskFactory(CancellationToken.None).IsCanceled);

However, the compiler (rightfully?) complains: warning CS1998: This async method lacks 'await' operators and will run synchronously. I could not figure out a way to transform this into a synchronous variant. I tried these 2 options for the lambda

  1. without async: token => throw new OperationCanceledException(). It's pretty clear to me that this will just throw the exception directly on the stack instead of wrapping it in the task, but this is what the IDE suggested.
  2. token => Task.FromException(new OperationCanceledException()). This goes to IsFaulted instead of IsCanceled.

What is the correct way to transfrom this to a synchronous variant?

CodePudding user response:

You could await a completed task:

Func<CancellationToken, Task> taskFactory = async token =>
{
    await Task.CompletedTask;
    throw new OperationCanceledException();
};
        
Assert.True(taskFactory(CancellationToken.None).IsCanceled);

However your test does have a race condition, although it's likely to usually succeed. But if you change it to this:

Func<CancellationToken, Task> taskFactory = async token =>
{
    await Task.Delay(1000);
    throw new OperationCanceledException();
};

var task = taskFactory(CancellationToken.None);
Console.WriteLine(task.IsCanceled);

it will print false. To fix that, you have to wait for the task to complete:

Func<CancellationToken, Task> taskFactory = async token =>
{
    await Task.Delay(1000);
    throw new OperationCanceledException();
};

var task = taskFactory(CancellationToken.None);
Task.WaitAny(task);
Console.WriteLine(task.IsCanceled);

Note that I'm using Task.WaitAny() to wait for the task to complete without throwing a TaskCanceledException. If you simply await task; it will of course throw that exception.

CodePudding user response:

You could use the Task.FromCanceled method:

Func<CancellationToken, Task> taskFactory =
    token => Task.FromCanceled(new CancellationToken(true));

CodePudding user response:

You could use the low-level APIs as well to achieve the desired outcome in a synchronous way.

TaskCompletionSource tcs = new();
Func<CancellationToken, Task> taskFactory = token => tcs.Task;

With this approach your delegate remains sync (don't need to use the async keyword)

CancellationTokenSource cts = new(0);
cts.Token.Register(() => tcs.TrySetCanceled());

Here we connect the Task and Cancellation primitives via a simple callback.
We cancel the Task right away.

And that's it :) A working example can be found here.

  • Related