It's important to note a couple things:
async
methods start running synchronously. The magic happens at await
, if await
is given an incomplete Task
.
- Asynchronous != parallel. Running something asynchronously just lets the thread go and do something else while it is waiting for a reply from somewhere. It doesn't mean that multiple things are happening at the same time.
With those things in mind, this is what's happening in your case when it loops through all the tasks you've created:
- All of the tasks are put on the "to do" list.
- Task 1 is started.
- At
await
, an incomplete Task
is returned, and the rest of the method is put on the "to do" list.
- The thread realizes there is nothing to do and moves on to the next thing on the "to do" list, which is to start the next
Task
.
At step 4, the next thing on the "to do" list will always be the next Task
in the list until there is nothing left in the list. Only then, the next thing on the "to do" list is the continuation of the tasks that have completed, in the order they completed.
All of this happens on the same thread: it is asynchronous, not parallel.
But! If you actually use SQL calls (and you make a new connection for each task, since a single connection can only run one query at a time - unless you enable Multiple Active Result Sets) and monitor SQL, you will see those calls coming in and likely finishing before all of them have started, because SQL runs queries in parallel. It's only that the continuation of the C# methods won't start until all the tasks have started.
If you are truly looking to run these in parallel, then you need multi-threading. You can look at Parallel.ForEach
(examples here), but that is not asynchronous. It will create a thread for each instance and the thread will block until it's complete. That's not a big deal in a desktop app, but in ASP.NET, threads are finite, so you need to be careful.
There is a big discussion of this here, but I particularly like this answer, which is not multi-threading, but gives a way to throttle your tasks. So you can tell it to start x
number of tasks, and as each task finishes, start the next until all of them have run. For your code, that would look something like this (running 10 tasks at a time):
static async Task DatabaseCallsAsync()
{
List<int> inputParameters = new List<int>();
for (int i = 0; i < 100; i++)
{
inputParameters.Add(i);
}
await RunWithMaxDegreeOfConcurrency(10, inputParameters, x => DatabaseCallAsync($"Task {x}"));
}
static async Task DatabaseCallAsync(string taskName)
{
Console.WriteLine($"{taskName}: start");
await Task.Delay(1000);
Console.WriteLine($"{taskName}: finish");
}
public static async Task RunWithMaxDegreeOfConcurrency<T>(
int maxDegreeOfConcurrency, IEnumerable<T> collection, Func<T, Task> taskFactory)
{
var activeTasks = new List<Task>(maxDegreeOfConcurrency);
foreach (var task in collection.Select(taskFactory))
{
activeTasks.Add(task);
if (activeTasks.Count == maxDegreeOfConcurrency)
{
await Task.WhenAny(activeTasks.ToArray());
//observe exceptions here
activeTasks.RemoveAll(t => t.IsCompleted);
}
}
await Task.WhenAll(activeTasks.ToArray()).ContinueWith(t =>
{
//observe exceptions in a manner consistent with the above
});
}