Reputation: 95
I stumbled on something I can't really wrap my head around.
Example Code
Consider this example code.
public static void Main()
{
Example(false)
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
Console.ReadLine();
}
public static async Task Example(bool pause)
{
List<int> items = Enumerable.Range(0, 10).ToList();
DateTime start = DateTime.Now;
foreach(var item in items) {
await ProcessItem(item, pause);
}
DateTime end = DateTime.Now;
Console.WriteLine("using normal foreach: " + (end - start));
var tasks = items.Select(x => ProcessItem(x, pause));
start = DateTime.Now;
await Task.WhenAll(tasks);
end = DateTime.Now;
Console.WriteLine("using Task.WhenAll " + (end - start));
}
public static async Task ProcessItem(int item, bool pause)
{
Console.WriteLine($"[{item}]: invoked at " + DateTime.Now.ToString("hh:mm:ss.fff tt"));
if (pause) {
await Task.Delay(1);
}
int x = 5;
for (int i = 0; i < 1 * 1000000; i++) {
x = await Calculate(i);
}
}
public static async Task<int> Calculate(int item)
{
return await Task.FromResult(item + 5);
}
In the Example
method I simply call the ProcessItem
method, at first with a normal foreach and then with Task.WhenAll
.
ProcessItem
takes some number and a flag indicating whether await.TaskDelay(1)
should be invoked, more on that later. Besides that, all it does it simulate some longer running code + a call to a third awaitable method (Calculate
).
Result
The result when running the code is
[0]: invoked at 01:19:17.417
[1]: invoked at 01:19:17.898
[2]: invoked at 01:19:18.330
[3]: invoked at 01:19:18.782
[4]: invoked at 01:19:19.118
[5]: invoked at 01:19:19.472
[6]: invoked at 01:19:19.716
[7]: invoked at 01:19:19.961
[8]: invoked at 01:19:20.179
[9]: invoked at 01:19:20.402
using normal foreach: 00:00:03.2314927
[0]: invoked at 01:19:20.639
[1]: invoked at 01:19:20.887
[2]: invoked at 01:19:21.178
[3]: invoked at 01:19:21.440
[4]: invoked at 01:19:21.670
[5]: invoked at 01:19:21.954
[6]: invoked at 01:19:22.390
[7]: invoked at 01:19:22.880
[8]: invoked at 01:19:23.218
[9]: invoked at 01:19:23.449
using Task.WhenAll 00:00:03.0749655
The normal loop and Task.WhenAll
take about the same time for the execution, looks like both versions work sequentially because in both cases there's always some delay between the outputs.
Now let's make things weird.
If I pass true
instead of false
to Example
, the method now invokes await.TaskDelay(1)
, resulting in a different execution as you can see in the result.
[0]: invoked at 01:22:17.047
[1]: invoked at 01:22:17.521
[2]: invoked at 01:22:17.886
[3]: invoked at 01:22:18.337
[4]: invoked at 01:22:18.735
[5]: invoked at 01:22:19.024
[6]: invoked at 01:22:19.262
[7]: invoked at 01:22:19.500
[8]: invoked at 01:22:19.731
[9]: invoked at 01:22:19.992
using normal foreach: 00:00:03.2050316
[0]: invoked at 01:22:20.240
[1]: invoked at 01:22:20.241
[2]: invoked at 01:22:20.241
[3]: invoked at 01:22:20.241
[4]: invoked at 01:22:20.242
[5]: invoked at 01:22:20.242
[6]: invoked at 01:22:20.242
[7]: invoked at 01:22:20.243
[8]: invoked at 01:22:20.243
[9]: invoked at 01:22:20.244
using Task.WhenAll 00:00:01.4674985
As you can see, the normal loop works as usual but apparently Task.WhenAll
now decides to call the ProcessItem
method for all items at the same time - whereas before, one item after the other was being processed.
Questions
Why does executing await Task.Delay(1)
make such a huge difference?
Why doesn't the first version (where await Task.Delay(1)
wasn't being called) call the ProcessItem
for all items at around the same time?
It seems like I'm missing something here. I've tested the code with .NET 4.5 and .NET 4.7.2 - same results.
Upvotes: 3
Views: 925
Reputation: 39274
When you have await MyAsyncMethod()
, that MyAsyncMethod returns a Task
that may or may not be in an IsCompleted state. And the execution of the rest of the containing method depends on that state.
In your Calculate
method, you are returning Task.FromResult
, which is a completed task. Task.Delay however is not completed until the timeout passes, so you immediately get an incomplete Task.
So with pause==false
, your methods run in effect synchronously and all have completed when the foreach is finished, leaving nothing for Task.WhenAll to wait on.
With pause==true
, the ProcessItem
method returns an incomplete task as soon as the Delay is hit. So the several invocations of this method are started rapidly (you see the Console.WriteLine output close together in time) and only after the delays have expired, is the rest executed - in the Task.WhenAll
.
Upvotes: 5