Celeste Soueid
Celeste Soueid

Reputation: 318

Task.waitAny functionality for awaitables

I'm currently running into a race condition due to waiting for 0.25 seconds in an async function. A cancellation token is checked in the loop, but happens late due to the wait.

I'm using a library that provides 'Await.Until' functionality, which allows for awaiting boolean values. As such I considered waiting for either 0.25 seconds, or token.isCancelled to be true.

I'm sure outsourcing them to separate tasks, and then using Task.waitAny() would work, but it seems a little over the top. Is there a more elegant way to await multiple awaitables?

while (!Completed)
{
    Completed = AdjustPath();

    await Await.Seconds(PathingDelay);

    if (!loop) 
        return Completed;
    else if (token.IsCancellationRequested)
    {
        StopMove();

        return false;
    }
}

The issue above is that the containing function is called again, which sets a destination, before the cancellation is resolved, which stops the object and removes the destination. While I could await the task finishing, that would add a delay to the method running, which is not ideal.

Upvotes: 0

Views: 239

Answers (2)

Sir Rufo
Sir Rufo

Reputation: 19106

With a TaskCompletionSource<T> you can build a Task that will complete when a cancellation is requested by a given CancellationToken.

public static class Async
{
    private static readonly TaskCompletionSource<bool> _neverComplete = new TaskCompletionSource<bool>();

    public static Task CompleteOnCancellation( CancellationToken token )
    {
        if ( !token.CanBeCanceled )
            return _neverComplete.Task;
        if ( token.IsCancellationRequested )
            return Task.CompletedTask;

        var tcs = new TaskCompletionSource<bool>();

        token.Register( () => tcs.SetResult( true ) );

        return tcs.Task;
    }
}

Now you can use Task.WhenAny to interrupt the waiting delay

using var cts = new CancellationTokenSource( 100 );

var longWaitingTask = Task.Delay( 250 );
var cancellationTask = Async.CompleteOnCancellation( cts.Token );

var completedTask = await Task.WhenAny( longWaitingTask, cancellationTask );

if ( completedTask == longWaitingTask )
{
    Console.WriteLine( "Long Running Task completed." );
}
if ( completedTask == cancellationTask )
{
    Console.WriteLine( "Cancellation Task completed." );
}

Upvotes: 1

Michael Gehling
Michael Gehling

Reputation: 647

Use Delay(TimeSpan, CancellationToken), like Dmitry already proposed in his comment.

while (!Completed)
{
    Completed = AdjustPath();

    try
    {
        await Task.Delay(PathingDelay, token);
    }
    catch(TaskCanceledException e){} // if delay was cancelled is checked in next lines

    if (!loop) 
        return Completed;
    else if (token.IsCancellationRequested)
    {
        StopMove();

        return false;
    }
}

Upvotes: 0

Related Questions