Why does running a hundred async tasks take longer than running a hundred threads?
I have the following test class:
public class AsyncTests
{
public void TestMethod1()
{
var tasks = new List<Task>();
for (var i = 0; i < 100; i++)
{
var task = new Task(Action);
tasks.Add(task);
task.Start();
}
Task.WaitAll(tasks.ToArray());
}
public void TestMethod2()
{
var threads = new List<Thread>();
for (var i = 0; i < 100; i++)
{
var thread = new Thread(Action);
threads.Add(thread);
thread.Start();
}
foreach (var thread in threads)
{
thread.Join();
}
}
private void Action()
{
var task1 = LongRunningOperationAsync();
var task2 = LongRunningOperationAsync();
var task3 = LongRunningOperationAsync();
var task4 = LongRunningOperationAsync();
var task5 = LongRunningOperationAsync();
Task[] tasks = {task1, task2, task3, task4, task5};
Task.WaitAll(tasks);
}
public async Task<int> LongRunningOperationAsync()
{
var sw = Stopwatch.StartNew();
await Task.Delay(500);
Debug.WriteLine("Completed at {0}, took {1}ms", DateTime.Now, sw.Elapsed.TotalMilliseconds);
return 1;
}
}
As far as can tell, TestMethod1
and TestMethod2
should do exactly the same. One uses TPL, two uses plain vanilla threads. One takes 1:30 minutes, two takes 0.54 seconds.
Why?
The Action
method is currently blocking with the use of Task.WaitAll(tasks)
. When using Task
by default the ThreadPool
will be used to execute, this means you are blocking the shared ThreadPool
threads.
Try the following and you will see equivalent performance:
Add a non-blocking implementation of Action
, we will call it ActionAsync
private Task ActionAsync()
{
var task1 = LongRunningOperationAsync();
var task2 = LongRunningOperationAsync();
var task3 = LongRunningOperationAsync();
var task4 = LongRunningOperationAsync();
var task5 = LongRunningOperationAsync();
Task[] tasks = {task1, task2, task3, task4, task5};
return Task.WhenAll(tasks);
}
Modify TestMethod1
to properly handle the new Task
returning ActionAsync
method
public void TestMethod1()
{
var tasks = new List<Task>();
for (var i = 0; i < 100; i++)
{
tasks.Add(Task.Run(new Func<Task>(ActionAsync)));
}
Task.WaitAll(tasks.ToArray());
}
The reason you were having slow performance is because the ThreadPool
will "slowly" spawn new threads if required, if you are blocking the few threads it has available, you will encounter a noticeable slowdown. This is why the ThreadPool
is only intended for running short tasks.
If you are intending to run a long blocking operation using Task
then be sure to use TaskCreationOptions.LongRunning
when creating your Task
instance (this will create a new underlying Thread
rather than using the ThreadPool
).
Some further evidence of the ThreadPool
being the issue, the following also alleviates your issue (do NOT use this):
ThreadPool.SetMinThreads(500, 500);
This demonstrates that the "slow" spawning of new ThreadPool
threads was causing your bottleneck.