I love PLINQ. In all of my test cases on various parallelization patterns, it performs well and consistently. But I recently ran into a question that has me a little bothered: what is the functional difference between these two examples? What, hopefully if anything, qualifies the PLINQ example to not be similar or equivalent to the following anti-pattern?
PLINQ:
public int PLINQSum()
{
return Enumerable.Range(0, N)
.AsParallel()
.Select((x) => x + 1)
.Sum();
}
Sync over async:
public int AsyncSum()
{
var tasks = Enumerable.Range(0, N)
.Select((x) => Task.Run(() => x + 1));
return Task.WhenAll(tasks).Result.Sum();
}
The
AsyncSummethod is not an example of Sync over Async. It is an example of using theTask.Runmethod with the intention of parallelizing a calculation. You might think thatTask= async, but it's not. TheTaskclass 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
AsyncSummethod as anti-pattern might be fair, but not because it is Sync over Async. You might want to call it anti-pattern because:Taskfor each number in the sequence, incurring a gigantic overhead compared to the tiny computational work that has to be performed.ThreadPoolfor the whole duration of the parallel operation.ThreadPoolto create additional threads, resulting in oversubscription (more threads than CPUs). This results in the operating system having more work to do (switching between threads).ThreadPoolthreads. 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 theTaskconstructor (instead ofTask.Run), and then use theRunSynchronouslymethod in order to run them on the current thread, while the rest of the tasks are scheduled on theThreadPool.The name
AsyncSumitself is inappropriate, since there is nothing asynchronous happening inside this method. A better name could beWhenAll_TaskRun_Sum.