public async Task<int> MyMethodAsync(Item item)
{
Thread.Sleep(1000); // This is just a placeholder method
return 0; // placeholder int
}
This method, as you have written it, is solely CPU bound. It keeps the CPU busy and as such requires “full attention” of a thread in order for it be processed. It being marked as async
and it returning a Task
does not mean that there is anything happening asynchronously here. So this mostly equivalent to the following fully synchronous method:
public Task<int> MyMethod(Item item)
{
Thread.Sleep(1000);
return Task.FromResult(0); // this is synchronous!
}
So when you execute this method, it blocks the CPU for that one second and then returns something which happens to be a completed task.
Now, looking at how you are running your test, this also explains what you are seeing. Let’s take the second implementation first:
foreach (Item item in items)
{
Tasks.Add(MyMethodAsync(item));
}
As we realized, MyMethodAsync
is fully synchronous. So executing the method here blocks the loop from continuing to the next item before the method returns. That means that for 10 items, the loop completes in approximately 10 seconds and the list will only contain completed tasks then (which will be awaited on instantly).
Compare this to the first implementation:
foreach (Item item in items)
{
Tasks.Add(Task.Run(() => MyMethodAsync(item)));
}
Task.Run
is an easy way to offload something onto a thread from the thread pool. This means that you basically create a job for some other thread to execute in the beackground. And Task.Run
gives you a task that represents the completion of that job. Since you only call MyMethodAsync
inside the lambda function that you pass to Task.Run
, this will not block the loop itself. So in the end, this will create 10 individual jobs that will be executed on other threads which will then happen to call the synchronous MyMethodAsync
.
Instead of blocking the execution of your main thread which is executing the loop, you are now blocking some thread pool threads. Those threads run concurrently so this will be faster than making each synchronous call in sequence on the main thread. That is why using Task.Run
here yields faster results.
That all being said, when you are using Task.Run
for this, you are still blocking some threads. Since thread pool threads are generally limited, you usually want to avoid this. Of course, there are exceptions and using Task.Run
for CPU-bound work to offload it to a background thread is generally okay, but you should still be careful here since it’s not uncommon that you run out of available threads.
Instead, you should try to do actual asynchronous work here. When something is truly asynchronous, that means that the thread executing the method is released to do something else until the asynchronous work is complete. That means that you are reusing threads more effectively and generally don’t block for things that don’t need to block.
If you replace your method with an asynchronous implementation, you can see this in effect here:
public async Task<int> MyMethodAsync(Item item)
{
await Task.Delay(1000);
return 0;
}
Task.Delay
is truly asynchronous, so this method will not block the thread that called it. This should mean that none of your two implementations will block and both will just start the work while then waiting for it to complete.
I’ve tested this with 10000 items and basically saw no real difference. I had a difference of up to 20 milliseconds (in either direction) but I am willing to ignoring this due to not having a good benchmark environment for this particular test.
If anything, using Task.Run
here, with an asynchronous method, should be slightly less efficient because it still schedules a thread to run the asynchronous method (meaning that scheduled thread will be released immediately). So there is a slight overhead—which still can be ignored in most applications.