Codementor Events

Multithreaded Programming Techniques in C#

Published Jun 07, 2018Last updated Dec 04, 2018

The age of single core CPUs in computing is long gone. As processing power increases and multiple processing cores get stacked into a single silicon chip, the applications we write too need to be optimized to utilize the underlying hardware it runs on.
Though we overlook at times and focus ourselves more on the business logic implemented, in the long run in terms of usability and scalability applications would need to be revised and thought through for better optimization.

Multithreading however helps in solving the following.

  • Improved responsiveness of applications.
  • Maximize utilization and performance.
  • Concurrent access to resources.

Even though this may seem overwhelming for starters, the .NET framework consists of a great number of tools in its arsenal to support developers in multithreaded programming. At the time of this blog post C# 5.0 has introduced furthermore new additions to support asynchronous programming.

Approaches of Multithreaded Development

.NET provides the following techniques for multithreaded applications.

  1. Using the Thread class
  2. Using ThreadPools
  3. Using Background Workers in desktop application
  4. Using the Task Parallel Library
    ThreadingInfoGraphSummary of Thread types.

Simulation Code

To demonstrate the performance differences and the differences in implementation, we would be using the stopwatch class to calculate the time elapsed for each task. The time taken would then be compared to get a better understanding of the approaches discussed.

In order to simulate a fairly long running task, we would be calculating the factorials from 1 till 100 repeatedly for 5000 times. The 5000 iterations would be divided equally among the number of processes defined. This done so that the single threaded mode and its multi threaded variants would yield in the same number of iterations and would have carried out the same computation complexity. The basic simulation setup code is listed below, for simplicity the specific methods for each threading approach is extracted separately and discussed.

ThreadingExamples.Program.cs : Complex Calculation!

#region Fields
    /// <summary>
    /// Exceution Mode of the application defining whether it should run as multithreaded
    /// or single threaded.
    /// </summary>
    enum Mode
    {
        MULTITHREAD,
        THREADPOOL,
        TASK,
        SINGLETHREAD,
        BACKGROUNDWORKER
    }
 
    /// <summary>
    /// The number of threads to be spawned.
    /// </summary>
    private const int threadCount = 8;
 
    /// <summary>
    /// The total number of spins the actual work is carried out repeatedly.
    /// </summary>
    private const int totalCount = 5000;
    private const String fileName = "FactResult";
 
    #region Error Messages
    private const string invalidMode = "Invalid execution mode : must be either singlethread/multithread";
    private const string invalidArguments = "Enter the execution mode : singlethread/multithread";
    #endregion
    #endregion
 
    #region Methods
 
    /// <summary>
    /// Entry point of the application
    /// </summary>
    /// <param name="args">Arguments expecting the execution mode of the application (SingleThreaded | MultiThreaded)</param>
    static void Main(string[] args)
    {
        Thread.CurrentThread.Priority = ThreadPriority.Highest;
        Stopwatch watch = new Stopwatch();
        watch.Start();
 
        try
        {
            Mode mode = ValidateExtractInputArguments(args);
 
            HandleMode(mode);
 
            watch.Stop();
            Console.WriteLine("Elapsed Time in Milliseconds : \t "+watch.ElapsedMilliseconds);
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.ReadLine();
    }
 
    /// <summary>
    /// Performs the task based on the use defined mode argument.
    /// </summary>
    /// <param name="mode">The mode of execution of the application</param>
    private static void HandleMode(Mode mode)
    {
        switch (mode)
        {
            case Mode.MULTITHREAD:
                RunThreadMode();
                break;
            case Mode.SINGLETHREAD:
                ComplexWork(totalCount);
                break;
            case Mode.THREADPOOL:
                RunInThreadPool();
                break;
            case Mode.TASK:
                RunTaskMode();
                break;
            case Mode.BACKGROUNDWORKER:
                RunInBackgroundWorker();
                break;
            default:
                break;
        }
    }
 
    /// <summary>
    /// Validates and extracts the command line arguments.
    /// </summary>
    /// <param name="args">The list of arguments passed.</param>
    /// <returns>Returns the extracted mode.</returns>
    private static Mode ValidateExtractInputArguments(string[] args)
    {
        if (args.Length == 0)
        {
            throw new Exception(invalidArguments);
        }
        else
        {
            try
            {
                return (Mode)Enum.Parse(typeof(Mode), args[0], true);
            }
            catch (ArgumentException a)
            {
                throw new Exception(invalidMode, a);
            }
        }
    }
    #region Work Task
    /// <summary>
    /// The CPU intensive task to be carried out.
    /// </summary>
    /// <param name="n">Number of spins to iterate the task.</param>
    private static void ComplexWork(int n)
    {
        for (int j = 0; j < n; j++)
        {
            for (int i = 1; i < 100; i++)
            {
                Fac(i);
            }
        }
 
    }
 
    /// <summary>
    /// Calculate the factorial of a number.
    /// </summary>
    /// <param name="n">Number to calculate the factorial.</param>
    /// <returns>Return the factorial of the number sent.</returns>
    private static double Fac(double n)
    {
        if (n > 1)
        {
            return n * Fac(n - 1);
        }
        else
        {
            return 1;
        }
    }
 
    #endregion

Notice however that we have excluded Console.WriteLine or any form of IO operations since these could cause race conditions when accessing IO resources such as the Console, Network stream or File streams, and would cause deviations in the final elapsed time calculation.

Using the Thread class

The thread class provides the rudimentary blocks for creating multi-threaded applications. The following code explains how new threads could be spawned.

To begin with an array of threads are maintained and each thread is passed a lambda expression executing the same code block. Notice how we have greater control when using the Thread class itself, where it allows us to change settings such as priority of a thread. Its important to understand that threads constructed from the Thread class are by default foreground threads, which means that the main thread would not complete until all foreground threads spawned marks its completion. However we must still join these threads on the main thread so that we could calculate the elapsed time at and display it sequentially once all of the threads are completed.

ThreadingExamples.Program.cs : Using Threads

/// <summary>
/// Spawns new threads based on the thread count and starts the activity.
/// </summary>
private static void RunThreadMode()
{
 
    Thread[] t = new Thread[threadCount];
 
    for (int i = 0; i < threadCount; i++)
    {
        t[i] = new Thread(() =>
        {
            ComplexWork(totalCount / threadCount);
        });
        t[i].Priority = ThreadPriority.Highest;
        t[i].Start();
 
    }
 
    // Waits for all the threads to finish.
    foreach (var ct in t)
    {
        ct.Join();
    }
}

Using ThreadPools

Even though creating threads as above is fairly simple, it has its one drawbacks. Usually creating and disposing multiple threads during a lifespan of an application is resource intensive. The Operating system has quite a lot in hand to perform when a thread is created. As a solution for this an automated thread pool is provided in .NET. As the name implies the pool could hold a range of pre-created threads un-disposed which would be readily available.

When using the thread pool we do not need to start the thread manually. The approach differs such that a task is been queued and whenever a thread is available in the queue .NET itself would assign the task and execute it. Therefore there isn’t a guarantee that a thread would execute immediately when queued in a thread pool.

Another important thing to notice is that unlike when using the thread class, there is no specific object to join to the main thread. Therefore we have to use a signaling mechanism to let the main thread know that the task have been completed. The CountdownEvent helps in signaling, and if its not used since Thread pool threads are background threads the application would terminate prior to the execution of the pool threads.

ThreadingExamples.Program.cs : Using Threads

/// <summary>
/// Executes the task in a thread pooling context.
/// </summary>
private static void RunInThreadPool()
{
    using (CountdownEvent signaler = new CountdownEvent(threadCount))
    {
        for (int i = 0; i < threadCount; i++)
        {
            ThreadPool.QueueUserWorkItem((x) => { 
                ComplexWork(totalCount / threadCount);
                signaler.Signal();
            });
        }
        signaler.Wait();
    }
 
}

Using Background Workers in desktop application

The background worker defines a DoWork event handler which we could bind to start the work. Since event handlers are involved, the background worker is more time consuming compared to the other approaches. The same signaling method is used here.

ThreadingExamples.Program.cs : Using Background Workers

/// <summary>
 /// Start background workers to perform the same action
 /// </summary>
 private static void RunInBackgroundWorker()
 {
     BackgroundWorker[] backgroundWorkerList = new BackgroundWorker[threadCount];
     using (CountdownEvent signaler = new CountdownEvent(threadCount))
     {
         for (int i = 0; i < threadCount; i++)
         {
             backgroundWorkerList[i] = new BackgroundWorker();
             backgroundWorkerList[i].DoWork += delegate(object sender, DoWorkEventArgs e)
             {
                 ComplexWork(totalCount / threadCount);
                 signaler.Signal();
             };
             backgroundWorkerList[i].RunWorkerAsync();
         }
         signaler.Wait();
     }
 
 }

Using the Task Parallel Library

A Task represents an asynchronous task. This is could be considered a more high-level approach than using the Thread class.

ThreadingExamples.Program.cs : Using the Task Parallel Library

// <summary>
/// Creates a new task based on the TPL library.
/// </summary>
private static void RunTaskMode()
{
    Task[] taskList = new Task[threadCount];
    for (int i = 0; i < threadCount; i++)
    {
        taskList[i] = new Task(new Action(() =>
        {
            ComplexWork(totalCount / threadCount);
        }));
        taskList[i].Start();
    }
    Task.WaitAll(taskList);
} 

Complete Example

To differentiate between different threading techniques the following code setup would be used and built upon as each technique is discussed.

ThreadingExamples.Program.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ThreadingExamples
{
    class Program
    {
        #region Fields
        /// <summary>
        /// Exceution Mode of the application defining whether it should run as multithreaded
        /// or single threaded.
        /// </summary>
        enum Mode
        {
            MULTITHREAD,
            THREADPOOL,
            TASK,
            SINGLETHREAD,
            BACKGROUNDWORKER
        }
 
        /// <summary>
        /// The number of threads to be spawned.
        /// </summary>
        private const int threadCount = 8;
 
        /// <summary>
        /// The total number of spins the actual work is carried out repeatedly.
        /// </summary>
        private const int totalCount = 5000;
        private const String fileName = "FactResult";
 
        #region Error Messages
        private const string invalidMode = "Invalid execution mode : must be either singlethread/multithread";
        private const string invalidArguments = "Enter the execution mode : singlethread/multithread";
        #endregion
        #endregion
 
        #region Methods
 
        /// <summary>
        /// Entry point of the application
        /// </summary>
        /// <param name="args">Arguments expecting the execution mode of the application (SingleThreaded | MultiThreaded)</param>
        static void Main(string[] args)
        {
            Thread.CurrentThread.Priority = ThreadPriority.Highest;
            Stopwatch watch = new Stopwatch();
            watch.Start();
 
            try
            {
                Mode mode = ValidateExtractInputArguments(args);
 
                HandleMode(mode);
 
                watch.Stop();
                Console.WriteLine("Elapsed Time in Milliseconds : \t "+watch.ElapsedMilliseconds);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            Console.ReadLine();
        }
 
        /// <summary>
        /// Performs the task based on the use defined mode argument.
        /// </summary>
        /// <param name="mode">The mode of execution of the application</param>
        private static void HandleMode(Mode mode)
        {
            switch (mode)
            {
                case Mode.MULTITHREAD:
                    RunThreadMode();
                    break;
                case Mode.SINGLETHREAD:
                    ComplexWork(totalCount);
                    break;
                case Mode.THREADPOOL:
                    RunInThreadPool();
                    break;
                case Mode.TASK:
                    RunTaskMode();
                    break;
                case Mode.BACKGROUNDWORKER:
                    RunInBackgroundWorker();
                    break;
                default:
                    break;
            }
        }
 
        /// <summary>
        /// Validates and extracts the command line arguments.
        /// </summary>
        /// <param name="args">The list of arguments passed.</param>
        /// <returns>Returns the extracted mode.</returns>
        private static Mode ValidateExtractInputArguments(string[] args)
        {
            if (args.Length == 0)
            {
                throw new Exception(invalidArguments);
            }
            else
            {
                try
                {
                    return (Mode)Enum.Parse(typeof(Mode), args[0], true);
                }
                catch (ArgumentException a)
                {
                    throw new Exception(invalidMode, a);
                }
            }
        }
 
        #region Work Task
        /// <summary>
        /// The CPU intensive task to be carried out.
        /// </summary>
        /// <param name="n">Number of spins to iterate the task.</param>
        private static void ComplexWork(int n)
        {
            for (int j = 0; j < n; j++)
            {
                for (int i = 1; i < 100; i++)
                {
                    Fac(i);
                }
            }
 
        }
 
        /// <summary>
        /// Calculate the factorial of a number.
        /// </summary>
        /// <param name="n">Number to calculate the factorial.</param>
        /// <returns>Return the factorial of the number sent.</returns>
        private static double Fac(double n)
        {
            if (n > 1)
            {
                return n * Fac(n - 1);
            }
            else
            {
                return 1;
            }
        }
 
        #endregion
 
        /// <summary>
        /// Spawns new threads based on the thread count and starts the activity.
        /// </summary>
        private static void RunThreadMode()
        {
 
            Thread[] t = new Thread[threadCount];
 
            for (int i = 0; i < threadCount; i++)
            {
                t[i] = new Thread(() =>
                {
                    ComplexWork(totalCount / threadCount);
                });
                t[i].Priority = ThreadPriority.Highest;
                t[i].Start();
 
            }
 
            // Waits for all the threads to finish.
            foreach (var ct in t)
            {
                ct.Join();
            }
        }
 
        /// <summary>
        /// Executes the task in a thread pooling context.
        /// </summary>
        private static void RunInThreadPool()
        {
            using (CountdownEvent signaler = new CountdownEvent(threadCount))
            {
                for (int i = 0; i < threadCount; i++)
                {
                    ThreadPool.QueueUserWorkItem((x) =>
                    {
                        ComplexWork(totalCount / threadCount);
                        signaler.Signal();
                    });
                }
                signaler.Wait();
            }
 
        }
 
        /// <summary>
        /// Creates a new task based on the TPL library.
        /// </summary>
        private static void RunTaskMode()
        {
            Task[] taskList = new Task[threadCount];
            for (int i = 0; i < threadCount; i++)
            {
                taskList[i] = new Task(new Action(() =>
                {
                    ComplexWork(totalCount / threadCount);
                }));
                taskList[i].Start();
            }
            Task.WaitAll(taskList);
        }
 
        /// <summary>
        /// Start background workers to perform the same action
        /// </summary>
        private static void RunInBackgroundWorker()
        {
            BackgroundWorker[] backgroundWorkerList = new BackgroundWorker[threadCount];
            using (CountdownEvent signaler = new CountdownEvent(threadCount))
            {
                for (int i = 0; i < threadCount; i++)
                {
                    backgroundWorkerList[i] = new BackgroundWorker();
                    backgroundWorkerList[i].DoWork += delegate(object sender, DoWorkEventArgs e)
                    {
                        ComplexWork(totalCount / threadCount);
                        signaler.Signal();
                    };
                    backgroundWorkerList[i].RunWorkerAsync();
                }
                signaler.Wait();
            }
 
        }
 
        #endregion
    }
}

Lasitha Ishan Petthawadu

Discover and read more posts from Lasitha Ishan Petthawadu
get started