Rory Primrose

Learn from my mistakes, you don't have time to make them yourself

View project on GitHub

How do Parallel methods behave on a single core machine?

I’ve been wondering about this for a long time. None of the reading that I’ve done has conclusively answered this question. It just so happens that I’ve been developing in a VM for the last couple of months. I got the chance tonight to downgrade the VM specs to a single core to run some tests. The results were very pleasing, yet completely unexpected.

My test code is very unscientific but achieves the objective.

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
    
namespace ConsoleApplication1
{
    class Program
    {
        private static ConcurrentBag<int> _taskThreadIds =new ConcurrentBag<int>();
        private static ConcurrentBag<int> _parallelThreadIds =new ConcurrentBag<int>();
    
        static void Main(string[] args)
        {
            Console.WriteLine("Starting task array on {0}", Thread.CurrentThread.ManagedThreadId);
            Stopwatch watch = Stopwatch.StartNew();
    
            Task[] tasks = new Task[100];
    
            for (int i = 0; i < 100; i++)
            {
                tasks[i] = Task.Factory.StartNew(TaskAction, i);
            }
    
            Task.WaitAll(tasks);
    
            watch.Stop();
    
            OutputResults(_taskThreadIds, watch, "task array");
    
            Console.WriteLine("Starting parallel loop on {0}", Thread.CurrentThread.ManagedThreadId);
            watch = Stopwatch.StartNew();
    
            Parallel.For(0, 100, ParallelAction);
    
            watch.Stop();
    
            OutputResults(_parallelThreadIds, watch, "parallel");
    
            Console.WriteLine("Press key to close");
            Console.ReadKey();
        }
    
        private static void OutputResults(ConcurrentBag<int> ids, Stopwatch watch, string testType)
        {
            var allIds = ids.ToList();
            var uniqueIds = allIds.Distinct().ToList();
    
            Console.WriteLine("Completed {0} on {1} in {2} millseconds using {3} threads", testType,
                                Thread.CurrentThread.ManagedThreadId, watch.ElapsedMilliseconds, uniqueIds.Count);
    
            for (int i = 0; i < uniqueIds.Count; i++)
            {
                Console.WriteLine("Thread {0} was used {1} times", uniqueIds[i], allIds.Count(x => x == uniqueIds[i]));
            }
        }
    
        private static void TaskAction(object x)
        {
            _taskThreadIds.Add(Thread.CurrentThread.ManagedThreadId);
    
            //Console.WriteLine("{0}: Starting on {1}", x, Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(500);
            //Console.WriteLine("{0}: Completing on {1}", x, Thread.CurrentThread.ManagedThreadId);
        }
    
        private static void ParallelAction(int x)
        {
            _parallelThreadIds.Add(Thread.CurrentThread.ManagedThreadId);
    
            //Console.WriteLine("{0}: Starting on {1}", x, Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(500);
            //Console.WriteLine("{0}: Completing on {1}", x, Thread.CurrentThread.ManagedThreadId);
        }
    }
}

And here is the answer.image

The obvious observation is that Task.WaitAll on an array of tasks is almost three times slower. It uses a lower, but similar number of threads for the execution compared to Parallel. The most interesting outcome is the distribution of thread usage. This is where the real performance kicks in.

I was honestly expecting Parallel to not execute asynchronously in a single core environment as the documentation seems to be completely geared towards multi-core systems. I am really happy with this outcome, especially because the code flow makes better use of generics.

Written on March 26, 2013