Search code examples
c#async-awaittask-parallel-library.net-5

WhenAny behaving like WhenAll in certain case


So I had a problem with a third party library where the call could get stuck and never return even when calling cancellationToken.Cancel. The below is a prototype that take care of this situation and that it works.

public async Task MainAsync()
{
    try
    {
        await StartAsync().ConfigureAwait(false);
    }
    catch (Exception ex)
    {
        Console.WriteLine("Exception thrown");
    }
}

private async Task<string> StartAsync()
{
    var cts = new CancellationTokenSource();

    cts.CancelAfter(3 * 1000);

    var tcs = new TaskCompletionSource<string>();

    cts.Token.Register(() => { Console.WriteLine("Cancelled"); tcs.TrySetCanceled(); });

    return await (await Task.WhenAny(tcs.Task, LongComputationAsync())
        .ConfigureAwait(false)).ConfigureAwait(false);
}

private async Task<string> LongComputationAsync()
{
    await Task.Delay(1 * 60 * 1000).ConfigureAwait(false);

    return "Done";
}

So the Above will wait 3 seconds, and it will throw a TaskCancelledException like it should. If you then change the method LongComputationAsync to the following:

private Task<string> LongComputationAsync()
{
    Task.Delay(1 * 60 * 1000).ConfigureAwait(false).GetAwaiter().GetResult();

    return Task.FromResult("Done");
}

I would still expect this to have the same behaviour, but what this does is that, it will wait the full 1 minute (specified in the LongComputationAsync()) then throw the TaskCancelledException.

Can anyone explain this to me? On how this is working, or if this is the correct behaviour to begin with.


Solution

  • Can anyone explain this to me?

    Sure. The problem doesn't have anything to do with WhenAny. Rather, the problem is that the code assumes a method is asynchronous when it's synchronous.

    This is a relatively easy mistake to make. But as a general rule, a method with an asynchronous signature may be asynchronous; it does not have to be asynchronous.

    As I describe on my blog, asynchronous methods begin executing synchronously, just like synchronous methods. It is only when they hit an await that they may run asynchronously (and even then, they may continue synchronously).

    So, the new version of LongCompuationAsync is synchronous, and it executes the entire method before returning the task to StartAsync, which then passes it to WhenAny.