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?
Tasks + async / await are faster in this case than a pure multi threaded code. It's the simplicity which makes async / await so appealing.
Differences Between Task And ThreadThe Thread class is used for creating and manipulating a thread in Windows. A Task represents some asynchronous operation and is part of the Task Parallel Library, a set of APIs for running tasks asynchronously and in parallel. The task can return a result.
It is always advised to use tasks instead of thread as it is created on the thread pool which has already system created threads to improve the performance. The task can return a result. There is no direct mechanism to return the result from a thread.
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.
Tasks are executed on threads from the threadpool. The threadpool as a limited number of threads which are reused. All task, or all requested actions, are queued and executed by those threads when they are idle.
Let's assume your threadpool has 10 threads, and you have 100 tasks waiting, then 10 tasks are executed while the other 90 tasks are simply waiting in the queue untill the first 10 tasks are finished.
In the second testmethod you create 100 threads who are dedicated to their tasks. So instead of 10 threads running simultaniously, 100 threads are doing the work.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With