enzi
enzi

Reputation: 4165

Correct pattern to check if an async Task completed synchronously before awaiting it

I have a bunch of requests to process, some of which may complete synchronously. I'd like to gather all results that are immediately available and return them early, while waiting for the rest.

Roughly like this:

List<Task<Result>> tasks = new ();
List<Result> results = new ();

foreach (var request in myRequests) {
  var task = request.ProcessAsync();
  if (task.IsCompleted)
    results.Add(task.Result);  // or  Add(await task)  ?
  else 
    tasks.Add(task);
}

// send results that are available "immediately" while waiting for the rest
if (results.Count > 0)  SendResults(results);

results = await Task.WhenAll(tasks);
SendResults(results);

I'm not sure whether relying on IsCompleted might be a bad idea; could there be situations where its result cannot be trusted, or where it may change back to false again, etc.?

Similarly, could it be dangerous to use task.Result even after checking IsCompleted, should one always prefer await task? What if were using ValueTask instead of Task?

Upvotes: 1

Views: 1433

Answers (4)

spzvtbg
spzvtbg

Reputation: 1024

There are already good answers, but in addition of them here is my suggestion too, on how to handle multiple tasks and process each task differently, maybe it will suit your needs. My example is with events, but you can replace them with some kind of state management that fits your needs.

    public interface IRequestHandler
    {
        event Func<object, Task> Ready;
        Task ProcessAsync();
    }

    public class RequestHandler : IRequestHandler
    {
        // Hier where you wraps your request:
        // private object request;
        private readonly int value;

        public RequestHandler(int value)
            => this.value = value;

        public event Func<object, Task> Ready;

        public async Task ProcessAsync()
        {
            await Task.Delay(1000 * this.value);
            // Hier where you calls:
            // var result = await request.ProcessAsync();
            //... then do something over the result or wrap the call in try catch for example
            var result = $"RequestHandler {this.value} - [{DateTime.Now.ToLongTimeString()}]";

            if (this.Ready is not null)
            {
                // If result passes send the result to all subscribers

                await this.Ready.Invoke($"RequestHandler {this.value} - [{DateTime.Now.ToLongTimeString()}]");
            }
        }
    }

    static void Main()
    {
        var a = new RequestHandler(1);
        a.Ready += PrintAsync; 
        var b = new RequestHandler(2);
        b.Ready += PrintAsync;
        var c = new RequestHandler(3);
        c.Ready += PrintAsync;
        var d= new RequestHandler(4);
        d.Ready += PrintAsync;
        var e = new RequestHandler(5);
        e.Ready += PrintAsync;
        var f = new RequestHandler(6);
        f.Ready += PrintAsync;

        var requests = new List<IRequestHandler>()
        {
            a, b, c, d, e, f
        };

        var tasks = requests
            .Select(x => Task.Run(x.ProcessAsync));

        // Hier you must await all of the tasks
        Task
            .Run(async () => await Task.WhenAll(tasks))
            .Wait();
    }

    static Task PrintAsync(object output)
    {
        Console.WriteLine(output);

        return Task.CompletedTask;
    }

Upvotes: -1

StriplingWarrior
StriplingWarrior

Reputation: 156469

I'm not sure whether relying on IsCompleted might be a bad idea; could there be situations where its result cannot be trusted...

If you're in a multithreaded context, it's possible that IsCompleted could return false at the moment when you check on it, but it completes immediately thereafter. In cases like the code you're using, the cost of this happening would be very low, so I wouldn't worry about it.

or where it may change back to false again, etc.?

No, once a Task completes, it cannot uncomplete.

could it be dangerous to use task.Result even after checking IsCompleted.

Nope, that should always be safe.

should one always prefer await task?

await is a great default when you don't have a specific reason to do something else, but there are a variety of use cases where other patterns might be useful. The use case you've highlighted is a good example, where you want to return the results of finished tasks without awaiting all of them.

As Stephen Cleary mentioned in a comment below, it may still be worthwhile to use await to maintain expected exception behavior. You might consider doing something more like this:

var requestsByIsCompleted = myRequests.ToLookup(r => r.IsCompleted);

// send results that are available "immediately" while waiting for the rest
SendResults(await Task.WhenAll(requestsByIsCompleted[true]));
SendResults(await Task.WhenAll(requestsByIsCompleted[false]));

What if were using ValueTask instead of Task?

The answers above apply equally to both types.

Upvotes: 5

alexm
alexm

Reputation: 6882

Using only await you can achieve the desired behavior:


async Task ProcessAsync(MyRequest request, Sender sender)
{
     var result = await request.ProcessAsync();
     await sender.SendAsync(result);   
}

...

async Task ProcessAll()
{

   var tasks = new List<Task>();
   foreach(var request in requests) 
   {
      var task = ProcessAsync(request, sender);
      // Dont await until all requests are queued up
      tasks.Add(task);
   }
   // Await on all outstanding requests 
   await Task.WhenAll(tasks);

}



Upvotes: -1

John Glenn
John Glenn

Reputation: 1629

You could use code like this to continually send the results of completed tasks while waiting on others to complete.

foreach (var request in myRequests)
{
    tasks.Add(request.ProcessAsync());
}

// wait for at least one task to be complete, then send all available results
while (tasks.Count > 0)
{
    // wait for at least one task to complete
    Task.WaitAny(tasks.ToArray());

    // send results for each completed task
    var completedTasks = tasks.Where(t => t.IsCompleted);
    var results = completedTasks.Where(t => t.IsCompletedSuccessfully).Select(t => t.Result).ToList();
    SendResults(results);

    // TODO: handle completed but failed tasks here

    // remove completed tasks from the tasks list and keep waiting
    tasks.RemoveAll(t => completedTasks.Contains(t));
}

Upvotes: -1

Related Questions