1

The best I've been able to find in my research for a "do something with the results of tasks in a list as they complete" is this Microsoft page Process asynchronous tasks as they complete, unfortunately the example within accumulates the results of the tasks which I don't need to do; I only want to: spawn list of tasks, when a task completes, do something with its result.

So here I have adapted their example to demonstrate what I mean:

static async Task SumPageSizesAsync()
{
    IEnumerable<Task<int>> downloadTasksQuery =
        from url in s_urlList
        select ProcessUrlAsync(url, s_client);

    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        Console.WriteLine($"Just downloaded ${finishedTask} bytes");
    }
}

After any download task finishes, print an alert that it has finished (for example).

How can I more succinctly accomplish the same thing?

Is

downloadTasksQuery.Select(async t => Console.WriteLine($"Just downloaded ${await t} bytes"));

a good solution?

theonlygusti
  • 11,032
  • 11
  • 64
  • 119
  • *"when a task completes, do something with its result."* -- It's not only a matter of succinctness. Your current code guarantees that the continuations will run sequentially, one at a time, but not necessarily in order of completion (there is a bias for the completed tasks towards the left side of the list). Are you OK with executing the continuations concurrently to each other (in parallel)? – Theodor Zoulias Jan 24 '22 at 03:27
  • @TheodorZoulias yes, concurrent continuations are OK. Is the MS example already the most succinct non-concurrent continuations version? – theonlygusti Jan 24 '22 at 03:38
  • 2
    The more succinct non-concurrent way I know is to use the [`OrderByCompletion`](https://github.com/StephenCleary/AsyncEx/blob/0361015459938f2eb8f3c1ad1021d19ee01c93a4/src/Nito.AsyncEx.Tasks/TaskExtensions.cs#L184) extension method from Stepen Cleary's Nito.AsyncEx package. Example [here](https://stackoverflow.com/questions/62678367/task-whenall-but-process-results-one-by-one/62684221#62684221). It is also more efficient than the `WhenAny`-in-a-loop approach, and without bias. – Theodor Zoulias Jan 24 '22 at 03:56

2 Answers2

1

Yes, your approach is OK. Let's rewrite it in a less succinct way for clarity:

Task[] continuations = downloadTasksQuery
    .Select(async task =>
    {
        var result = await task;
        Console.WriteLine($"Just downloaded ${result} bytes")
    })
    .ToArray();

await Task.WhenAll(continuations);

The continuations will run on the current SynchronizationContext, if one exists, or on an unknown context otherwise. Without knowing the context, we can't say for sure if the continuations will be serialized or not.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

research for a "do something with the results of tasks in a list as they complete" is this Microsoft page Process asynchronous tasks as they complete,

As I describe in my book (section 2.6 "Processing Tasks as They Complete"), this is usually the wrong thing to search for.

Instead of thinking about the problem that way, compose the problem at a different level. Don't think about it as:

  • Take a list of inputs
  • Do asynchronous operation A on each input concurrently
  • As each A completes, do operation B on the result of A

Instead, compose A and B together, and then you have:

  • Take a list of inputs.
  • Do asynchronous operation A+B on each input concurrently

The resulting code is much cleaner:

static async Task SumPageSizesAsync()
{
  // Composition is done with async+await
  var tasks = s_urlList.Select(async url =>
  {
    var result = await ProcessUrlAsync(url, s_client);
    Console.WriteLine($"Just downloaded {result} bytes");
  }).ToList();

  // Asynchronous concurrency is done with Select + Task.WhenAll
  await Task.WhenAll(tasks);
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810