The AsyncSum
method is not an example of Sync over Async. It is an example of using the Task.Run
method with the intention of parallelizing a calculation. You might think that Task
= async, but it's not. The Task
class was introduced with the .NET Framework 4.0 in 2010, as part of the Task Parallel Library, two years before the advent of the async/await technology with the .NET Framework 4.5 in 2012.
What is Sync over Async: We use this term to describe a situation where an asynchronous API is invoked and then waited synchronously, causing a thread to be blocked until the completion of the asynchronous operation. It is implied that the asynchronous API has a truly asynchronous implementation, meaning that it uses no thread while the operation is in-flight. Most, but not all, of the asynchronous APIs that are built-in the .NET platform have truly asynchronous implementations.
The two examples in your question are technically different, but not because one of them is Sync over Async. None of them is. Both are parallelizing a synchronous operation (the mathematical addition x + 1
), that cannot be performed without utilizing the CPU. And when we use the CPU, we use a thread.
Characterizing the AsyncSum
method as anti-pattern might be fair, but not because it is Sync over Async. You might want to call it anti-pattern because:
- It allocates and schedules a
Task
for each number in the sequence, incurring a gigantic overhead compared to the tiny computational work that has to be performed.
- It saturates the
ThreadPool
for the whole duration of the parallel operation.
- It forces the
ThreadPool
to create additional threads, resulting in oversubscription (more threads than CPUs). This results in the operating system having more work to do (switching between threads).
- It has bad behavior in case of exceptions. Instead of stopping the operation as soon as possible after an error has occurred, it will invoke the lambda invariably for all elements in the sequence. As a result you'll have to wait for longer until you observe the error, and finally you might observe a huge number of errors.
- It doesn't utilize the current thread. The current thread is blocked doing nothing, while all the work is done by
ThreadPool
threads. In comparison the PLINQ utilizes the current thread as one of its worker threads. This is something that you could also do manually, by creating some of the tasks with the Task
constructor (instead of Task.Run
), and then use the RunSynchronously
method in order to run them on the current thread, while the rest of the tasks are scheduled on the ThreadPool
.
var task1 = new Task<int>(() => 1 + 1); // Cold task
var task2 = Task.Run(() => 2 + 2); // Hot task scheduled on the ThreadPool
task1.RunSynchronously(); // Run the cold task on the current thread
int sum = Task.WhenAll(task1, task2).Result.Sum(); // Wait both tasks
The name AsyncSum
itself is inappropriate, since there is nothing asynchronous happening inside this method. A better name could be WhenAll_TaskRun_Sum
.
Parallel.ForEach
vsTask.Run
withinforeach
, but this equally applies to PLINQ.Task.Run
in aSelect
, you are then working withTask<int>
objects and notint
values, so you cannot easily chain additional PLINQ operations (e.g.Where
) without waiting for those tasks to complete.