Disclaimer: I'm working on a MacBook so I'm using Visual Studio 2022 for Mac Preview, which does not support Tasks window (yet). So, I couldn't test my sample application
As I said in the comments section the you should explicitly define parent-child relationship between tasks otherwise they are just nested tasks.
- In the former case if one of the child tasks fails then the parent will fail as well
- In the latter case if one of the inner tasks fails then the outer will not fail
Rather than creating an infinite recursion I would suggest to create a sample application which simulates Task.WhenAll
var rnd = new Random();
var ids = Enumerable.Range(0, 4);
var whenAll = Task.Factory.StartNew(() =>
{
foreach (var id in ids)
{
var child = Task.Factory.StartNew((taskId) =>
{
Console.WriteLine($"Child task '{taskId}' is started");
Thread.Sleep(5000 + rnd.Next() % 1000);
Console.WriteLine($"Child task '{taskId}' is finished");
}, id, TaskCreationOptions.AttachedToParent);
}
});
await whenAll;
Console.WriteLine("Finished");
One possible output:
Child task '0' is started
Child task '1' is started
Child task '3' is started
Child task '1' is finished
Child task '3' is finished
Child task '0' is finished
Child task '2' is finished
Finished
Task.Factory.Start
instead of Task.Run
Please note that I've used Task.Factory.Start
to create the parent task. The reason for this is because the Task.Run
is equivalent of this:
Task.Factory.StartNew(action,
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default);
As you can see it explicitly defines that it denies attaching child tasks. So, it will not wait to its child tasks to finish.
In case of Task.Run
there is no overload which anticipates TaskCreationOptions
parameter.
Debugging
Pause the application after all child tasks have reported that they are started. At this point the Parent
column should be populated on the child task rows with the parent's task id.
UPDATE #1: I/O bound example
In the above example the child tasks were CPU-bound. Here is an example for I/O bound.
var client = new HttpClient();
var urls = new[]
{
"https://httpstat.us/200?sleep=5000",
"https://httpstat.us/200?sleep=4000",
"https://httpstat.us/200?sleep=4500",
"https://httpstat.us/200?sleep=3000",
};
async Task Get(string url)
{
Console.WriteLine($"Request to '{url}'");
await client.GetAsync(url);
Console.WriteLine($"Response from '{url}'");
};
var whenAll = Task.Factory.StartNew(() =>
{
foreach (var url in urls)
{
var child = Task.Factory.StartNew(
(requestUrl) => Get((string)requestUrl),
url,
TaskCreationOptions.AttachedToParent)
.Unwrap();
}
});
await whenAll;
Console.WriteLine("Finished");
- Since
StartNew
does not have overload which can accept a Func<Task>
that's why here the StartNew
returns a Task<Task>
- In order to flatten / unfold that we need to call
Unwrap
One possible output
Request to 'https://httpstat.us/200?sleep=5000'
Request to 'https://httpstat.us/200?sleep=3000'
Request to 'https://httpstat.us/200?sleep=4000'
Request to 'https://httpstat.us/200?sleep=4500'
Response from 'https://httpstat.us/200?sleep=3000'
Response from 'https://httpstat.us/200?sleep=4000'
Response from 'https://httpstat.us/200?sleep=4500'
Response from 'https://httpstat.us/200?sleep=5000'
Finished