Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / threads

Tasks and Task Parallel Library (TPL) : Multi-threading made easy

4.82/5 (58 votes)
9 Aug 2016CPOL31 min read 114.7K   1.6K  
Let's understand the evolution from older multi-threading world to Task Parallel library (alias TPL). What are the use cases where you would want to leverage TPL instead of creating threads on your own. What are exact areas where TPL has real edge.

Introduction

It had been really long since Tasks got released as part of .Net framework class library (FCL) as  a wrapper over thread-pool threads. Now Microsoft strongly recommends the usage of Tasks for almost all multithreaded background running work except in a very few special cases. So it makes perfect sense to understand as to why and how tasks are better in any way than using thread class directly. What are the advantages or new features from multi-threading point of view which we can leverage while using tasks. So a point wise comparison of threads and tasks is required to be proficient in using both of them.

A Note for the Attached Code

You need the following software as prerequisites to run the attached code on your computer:

  1. Visual Studio 2012 or above

I expect the reader to be well aware of following to be able to sail through this article comfortably:

  1. Basics of C# langauge
  2. Basics of creating and running a Windows forms application using .Net framework
  3. Basic knowledge of using Visual Studio 2012 (or higher version) IDE.
  4. Operating system concepts like thread scheduling and context switching.

Basic know how of threads, tasks and Task parallel library (TPL) in .Net will be an added advantage, although I'll try my level best to explain even the basic terms as and when they appear in the context. Since this is a comparative study between older threading world and the modern task world, so this article might not be a right fit for a 101 session to understand Tasks and Task Parallel Library from ground up. If you have not even heard the term task or TPL then I would suggest you to go through this MSDN article before continuing further with my article.

Background - The world of multi-threading on modern hardware

Since the times when we moved from DOS/Console based operating system (OS) to modern GUI based operating system we all know that multiple work can be done in parallel. In older days when there used to be only single core CPU then OS used to provide that logical impression that parallel work can be done by scheduling parallel running threads inside CPU and apply context switching among them using various types of scheduling algorithms for e.g. Round Robin. Now when modern hardware have come up with multiple core CPUs i.e. the number of cores inside the physical CPU present on the motherboard of your computer is more than 1, the OS's scheduling algorithm has got a real boost as it gets multiple physical CPUs in actual to execute work in parallel.

Note: Always remember having multiple CPUs and multiple Cores are different things. In case of multiple CPU you will have more than one physical CPU attached to your motherboard (not common in home PCs or desktop computers). In case of multiple core you will have SINGLE CPU attached to your motherboard but because of multiple cores your CPU can actually perform more than one instruction in parallel.

Multi-threaded programs - What does it really mean

Threads are the smallest unit of execution scheduled by the operating system. Any program or application has a minimum of one thread be it your ASP .Net application or a console window application. If you want to do work more fast you will have to employ multiple threads running in parallel but when there are more units of execution then there are more (I should have said endless) possibilities of making a mess due to wrong thread synchronization, data corruption, dead-locks, endless wait, unresponsive UI etc.

You ask any programmer and there is every possibility that he will have some or the other kind of fear with multithreaded programs. Single thread of execution always works like a charm but they take lot of time as there is single thread of execution responsible to perform each and every work happening in the system in a sequential fashion i.e. one after the other like a queue. There is no parallelism like in physical world you can drive a car and also take a cell phone call using your bluetooth hands free device. 

CPU Configuration of my computer

I have single CPU on my motherborad having four CPU cores as shown by the below snapshot. I'm in the performance tab of windows task manager. You can open windows task manager by pressing "Ctrl + Shift + Esc" key combination. As you can see, there are four blocks in CPU usage history section which represent four physical cores.

Image 1

Threads - The evolution in FCL

Good Old Days (.Net 1.1) :

If you have ever made even a basic multi-threaded program then we all end up using the Thread class in System.Threading namespace. What I will do in this article is to take a very basic problem and then try to evolve it from a normal threading application to a task (in TPL) based implementation by adding more and more requirements/constraints/features. Here is the problem statement given by one of my dear colleague in my office:

Let's begin to code

Problem Statement: I want to get the sum of first 1 million natural numbers as quickly as possible.

How would you solve it?

Solution # 1 (Very Basic) : The first very default way would be to sum all the elements sequentially using a for or while loop as shown in the code snippet below. This can be invoked by clicking the button with caption "Sum Using Simple For Loop" in the windows form application in the attached code.

C#
       private void btnSumUsingFor_Click(object sender, EventArgs e)
        {
            ClearResultLabel();
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            for (var i = 0 ; i < 1000000; i++)
            {
                _grandTotal += i + 1;
            }
            stopwatch.Stop();
            MessageBox.Show(String.Format("Sum total of all the numbers in the array is {0}. It took {1} miliseconds to perform this sum.", _grandTotal, stopwatch.ElapsedMilliseconds));
            //resetting sumTotal for next button click
            _grandTotal = 0;
        }

Then my friend told me that this sequential summation algorithm is very slow. I want to get the sum very fast. Really fast! 

Solution # 2 (Introducing Multi-threading): Then I started employing my brain which knows both algorithms as well as multi-threading. I came to a new solution to make the execution fast. I thought I can break the whole input in to ten equal  parts. Create the sub-total of numbers in ten separate parts parallely using ten separate threads. As soon as all ten threads are ready with partial sums then we can add their outputs to create the final sum. Let's do it in code. This function can be invoked by clicking button with caption "Multi-Threading without thread Synchronization" in the windows form application in the attached code.

C#
        private int _grandTotal;

        //Some code has been removed for brevity and clarity. 
        //Please download attached code for complete reference

        private void btnNormalThreading_Click(object sender, EventArgs e)
        {
            ClearResultLabel();
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            List<Thread> threadList = new List<Thread>();

            for (var i = 0L; i < 10; i++)
            {
                var threadStart = new ParameterizedThreadStart(AddNumbersWithoutThreadSynchronization);
                var thread = new Thread(threadStart);
                thread.Start(i * 100000 + 1);
                threadList.Add(thread);    
            }

            foreach (var curthread in threadList)
            {
                //wait for each thread to finish one by one.
                curthread.Join();
            }
            MessageBox.Show(String.Format("Sum total of all the numbers in the array is {0}. It took {1} miliseconds to perform this sum.", _grandTotal, stopwatch.ElapsedMilliseconds));
            //resettting sumTotal for next button click
            _grandTotal = 0;
        }

        private void AddNumbersWithoutThreadSynchronization(object lowerBound)
        {
            //Below line if uncommented will result in an invalidOperationException
            //as lblTotal is owned by the UI thread and you can not updated it from a background thread.
            //lblTotal.Text = "2";
            var subTotal = 0L;
            var counter = 0;
            long temp = (long)lowerBound;
            while (counter < 100000)
            {
                subTotal += temp;
                temp++;
                counter++;
            }
            _grandTotal += subTotal;
        }

        

If you run the application on your machine you should get the sum as 500000500000. If it is not the case then don't worry I'll sort this out as to why it is not happening, so just read on.

A Note about Performance: Simple and straight-forward solution. Isn't it? Let me set the record straight that in all likelyhood this solution will NOT generate a better performing application i.e. it is very much possible that time taken by this new solution in which we are implementing multi-threading can take longer time. The reason being the size and kind of task performed by individual threads. Here we are trying to do very trivial task of adding natural numbers for which a simple for loop might perform best. Since it is mostly arithmetic calculations, in all the work being done on 10 threads our dear CPU is aggressively involved. Such jobs like mathematical calculation where CPU is involved is called CPU-bound or CPU-intensive job.

There can be many other kind of jobs performed by your application e.g. I/O bound job in which you contact a device driver to read some data from disk, or a Network bound job in which you contact your NIC card driver to read some data over the web. In these IO-bound or Network bound tasks CPU is not involved at all and it sits idle. So when you have a mix of CPU bound and I/O bound work then few threads can do the CPU bound work while others can do the I/O bound work. In such a scenario, CPU has to schedule less number of threads in round-robin fashion which ultimately boosts performance because of reduction in the amount of an OS concept called context switching. You will soon see how.

  By the way I've taken this rudimentary example of sum of natural numbers just for demonstration purpose in this article. In real life when you actually solve complex problems e.g. processing multiple csv files present on disk (I/O bound) or making multiple remote web service calls (Network Bound) etc which are then performed on separate threads will give you the real benfit of parllelism.

So what are the reasons which can make even multi-threaded application a poor performing one:

  1. Overhead of thread creation:Creation of threads take execution time of your program. Newing up the Thread class is one such activity.
  2. Increased memory foot-print due to threads: Every thread is ultimately a data structure which need to save some information in main memory e.g. a thread maintains a stack for keeping CPU register values, program context data etc while it is executing. So it consumes space in your main memory. This results in higher memory foot print of your application as compared to a single threaded application.
  3. Costly context-switching: Context switching is an operating system (OS) concept in which your OS tries to give CPU slice to all the threads running in parallel as per a scheduling algorithm it is following. So all threads get CPU slice one by one and when a new thread has to grab the spot inside CPU, context switch occurs i.e. the execution context of current thread has to be removed and the execution context of the new thread has to be put in place before CPU can start real execution. During context switching the entire state of CPU, memory registers, stack state, CPU cache etc has to be saved for the current thread which is going to get pre-empted. So between the actual CPU cycles in which actual work is done there lies the cycle of context switching which also takes time. Though it is smaller as compared to actual CPU cycle it can really have an impact if context switching is happening very frequently if there are too many threads of same priority in your application.

You can see it for yourself in the picture below how an imaginary world of multi-threading would have looked without the cost of context switching involved. But OS will have to bear the cost of context switching in reality to give the multi-tasking feeling to the user so that he sees some progress after a second or two on every task he is doing in parallel. Here I've considered a case where there are only two threads. If you increase the number of threads in the system, the cost of context switching will only grow. 

Image 2

Well, moving on, first let's jot down the problem areas we have after converting our solution from single threaded to multi-threaded:

  1. Risky usage of global variables: Unfortunately when you use threads to schedule your computation work or a function, the return type of such a function should be void. .Net v3.5 or before programming paradigm doesn't allow you to assign a function returning some value to a thread for execution. So you end up using global variables to set some value if the called function intends to return something. Unfortunately global variables have global scope i.e. they are accessible to entire class and are suceptible to get misused and accidently overwritten by another thread as well. So if you were actually getting the correct sum of first 1 million natural numbers in Solution # 2 above then it is just that you were lucky. One wrong update of the global variable by any of the 10 threads spawned will give you wrong result.  We will see it why in next point.
  2. Thread Synchronization (The pit-fall of context switching): In most of multi-threaded programs you might have used shared resources. Like in our case _grandTotal variable is a shared resource which is accessible to all the threads of your application. Every thread being spun-off adds the sub-total of (1/10)th of a million integers in a cumulative (running total) fashion. Shared resources can always cause issues if they are not synchronized properly for multi-threaded environment. Like in this case, imagine if two threads finished their sub-totalling work simultaneously. Thread # 1 created a sub-total of the range it was asked to sum-up as subTotal1. It reads the value of currently value of _grandTotal as X. Now before thread # 1 can add the value of subTotal1 to X it got pre-empted from the CPU as per operating system's round-robin algorithm. Meanwhile Thread # 2 got CPU which again reads the value of _sumTotal as X. It created a sub-total of the range it was asked to sum-up as sumTotal2. Adds sumTotal2 to X which results in value Y and then writes the value Y back to _grandTotal. Then Thread # 1 again gets CPU slice, creates the sum of X (stale value of _sumTotal) and subTotal1 and writes it back to _grandTotal. So instead of having a value X + subTotal1 + subTotal2 for _sumTotal after two threads have finished execution we end up having X + subTotal1 because of thread synchronization issue. This messes up the output of the entire algorithm. You will see the solution # 3 in next section "Not all is bad in multi-threading world - The bright legacy from the past" which talks about thread synchronization construct to fix this issue.

  3. Unresponsive UI: Let's understand the main GUI thread's world responsible for the user interface with which any user interacts while interacting with an application. Below diagram depicts the pain of the main GUI thread in red.


    Image 3

    How often you have seen your application getting hung-up like below in case it is doing some work like getting a file from disk or getting a response from a web service on click of a button. It is a common scene in desktop applications. Look at the (Not Responding) text in the title bar of the application when I tried to move the window around using my mouse.

    Image 4


     Hung-up UI is always a problem in single threaded application when you use your GUI thread all the time for doing every damn work like getting a web response from remote server. Your application UI will get completely hung up as soon as your program enters into such requests as shown in the code below:

     
    C#
    private void btnLongRunningWork_Click(object sender, EventArgs e)
     {
         PerformLongWork();
     }
    
     private void PerformLongWork()
     {
         //this simulates some long work like a web service call or
         //reading a huge from from disk
         Thread.Sleep(5000);
         //do some long work here
     }
    

    In the mean time your web server responds, your user has to sit idle scratching his head with no option to click on your UI anywhere. You UI shows "(Not Responding)" as suffix text in the title bar of your program which represents a non-responsive UI. You can't click a button or interact with the UI of your program using mouse or keyboard in such a case.

    Two Important Scenarios

    Case 1: Even if you try employing conventional multi-threading in this case (see click event handler of button with caption "Long work with Threading") and hand-over the long running task to off-load your main GUI thread, it doesn't help. Why? Because just after handing over the work to be done to the new thread you've spawned yourself, you will call Join API on the new thread to wait for the completion of the long work. Now your GUI thread is not doing the heavy lifting job. But unforutnately it is not yet free! Have a look at the below code snippet:

     
    C#
            private void btnLongWorkWithThreading_Click(object sender, EventArgs e)
            {
                var threadStart = new ThreadStart(PerformLongWork);
                var thread = new Thread(threadStart);
                thread.Start();
                //Now main GUI thread starts waiting for the work to complete
                //GUI thread is still busy unable to do any real UI work
                thread.Join();
            }
    
            private void PerformLongWork()
            {
                //this simulates some long work
                Thread.Sleep(5000);
                //do some long work here
            }

    This essentially again results in a hung up UI because now your GUI thread is busy waiting for the other thread to finish instead of doing some real work. Your GUI thread is not free to be able to refresh your UI elements so that your UI remains interactive for the end-user. So you failed to make your UI responsive even after employing multi-threading :(. 

    Case 2 : Also, if instead of creating a thread yourself if you think of using CLR's thread-pool threads then your situation is even more painful. CLR's thread pool threads provide you no handle to call Join API on them so that you can wait for them to finish their work. So in such a case you will keep polling a global variable flag e.g. isThreadPoolWorkComplete using a while loop for state change which reflects the completion of work by Thread-Pool thread. Here is the code snippet. See click event handler of button with caption "Long work with thread pool"
    C#
            public bool isThreadPoolWorkComplete = false;
            private void btnLongWorkWithThreadpool_Click(object sender, EventArgs e)
            {
                ThreadPool.QueueUserWorkItem(PerformLengthyWork, null);
                //Now main GUI thread starts waiting for the work to complete
                //GUI thread is still busy unable to do any real UI work
                while (!isThreadPoolWorkComplete)
                {
                    Thread.Sleep(1000);
                    //keep waiting as threadpool thread has not finished its job
                }
            }
    
            private void PerformLengthyWork(object input)
            {
                //this simulates some long work like a web service call or 
                //reading a huge from from disk
                Thread.Sleep(5000);
                //do some long work here
            }

    So your main GUI thread will again be busy executing your while loop. So again, there is no way here to keep the main GUI thread free to respond to user events like a button click or refreshing the UI to show the progress bar for your work.
     
  4. Non-Cooperative Cancellation: My friend wanted to have a cancel button on the UI like in below snapshot which he will use to stop the program execution in case the sum operation is taking lot of time (Poor chap, he doesn't know he will never be able to even click a button as his UI will hang in such cases due to problem # 3 listed above :D).

    Image 5

    So if you have implemented the multi-threaded solution you will certainly want a way to stop/kill a thread to stop its execution. Threads support cancellation using its Abort API. Abort API is not that neat as it doesn't stop the execution of the thread in a neat fashion, instead CLR raises a ThreadAbortException to stop the execution which rips through the call stack of the function which is getting executed currently on that thread. Imagine an exception being raised for graceful termination of a work to stop execution. Sounds contradictory isn't it? But this is how it is implemented in .Net framework. You can read more knitty gritty details of this exception class here.

Not all is bad in multi-threading world - A bright legacy from the past

Solution # 3 (Multi-threading with thread synchronization) : Of course older multi-threading world had been a bit painful mostly because of thread-synchonization but atleast the problem of Risky Usage of Global variables can be solved in normal threading world itself by using thread synchronization constructs like Interlocked.Add or lock keyword. It goes without saying that even before tasks were invented the world of multi-threading had always dealt with problems like contention for shared resources, race conditions and deadlocks. In our case as we discussed in previous section how the shared resource _grandTotalis likely to be abused by threads running in parallel. Incrementing the global variable with partial sums is a critical event and ONLY one thread should be doing it at a time to avoid overwriting or stale reading.

So in case Solution # 2 was giving wrong output for you, you should check the algorithm used in click event handler of button with caption "Multi-Threading With Synchronization" which uses a Interlocked.Add thread synchronization construct for safe update of global variable _grandTotal. Here is the code snippet for your ready reference:

 

        private long _grandTotal;

        //Some code has been removed for brevity and clarity. 
       //Please download attached code for complete reference

        private void btnWithSync_Click(object sender, EventArgs e)
        {
            ClearResultLabel();
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            long counter = 0;
            List<Thread> threadList = new List<Thread>();

            for (var i = 0L; i < 10; i++)
            {
                var threadStart = new ParameterizedThreadStart(AddNumbersWithSynchronization);
                var thread = new Thread(threadStart);
                thread.Start(i * 100000 + 1);
                threadList.Add(thread);
            }

            foreach (var threadf in threadList)
            {
                //wait for each thread to finish one by one.
                threadf.Join();
            }
            stopwatch.Stop();
            MessageBox.Show(String.Format("Sum total of all the numbers in the array is {0}. It took {1} miliseconds to perform this sum.", _grandTotal, stopwatch.ElapsedMilliseconds));
            //resettting _grandTotal for next button click
            _grandTotal = 0;
        }

        private void AddNumbersWithSynchronization(object lowerBound)
        {
            var subTotal = 0L;
            var counter = 0;
            long temp = (long)lowerBound;
            while (counter < 100000)
            {
                subTotal += temp;
                temp++;
                counter++;
            }
            Interlocked.Add(ref _grandTotal, subTotal);
        }

A Note about Performance: Always remember thread synchronization also comes with a performance cost as now all threads can't just go and update the global variable blindly. Thread synchronization constructs make sure that the update happens only by one thread at a time. If two threads arrive at the same time then one of them will have to wait so that he sees the latest value updated by other thread before updating.

There is every possibility of this solution to be even more worse performing than the previous two as most of the job is CPU-bound which results in context switching of all 10 threads and you are using multi-threading with thread synchronization. So you pay the cost of all three - Creation and management of threads, CPU context switching and Thread Synchronization.

Introducing Tasks from TPL world

Now let's tame all the impediments one by one using tasks. Possibly I've not been able to quantify the real pain of conventional multi-threading world in few paragraphs but I'm sure once you start using tasks you will get a feel as to how much easier the things have become. In case you have used threads for long, then I'm sure you will echo my thoughts after finishing this article. A Task is a reference type found in System.Threading.Tasks namespace inside mscorlib.dll which is implicitly referenced in any C# project.

Now I will convert our multi-threaded solution # 2 into a program using Task available in task parallel library. After seeing the code we will again review our pain points to see if they are still there or gone with the wind :)  Here is the code snippet for the same:

C#
//That Global variable isn't missing from this code snippet. I actually didn't need them in my TPL 
//based imnplementaion using tasks.

//Some code has been removed for brevity and clarity. 
//Please download attached code for complete reference

private void btnWithTasks_Click(object sender, EventArgs e)
        {
            ClearResultLabel();
            long degreeofParallelism = 10;
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            long lowerbound = 0;
            long upperBound = 0;
            List<Task<long>> tasks = new List<Task<long>>();
            long countOfNumbersToBeAddedByOneTask = 100000; //1 lakh
            for (int spawnedThreadNumber = 1; spawnedThreadNumber <= degreeofParallelism; spawnedThreadNumber++)
            {
                lowerbound = ++upperBound;
                upperBound = countOfNumbersToBeAddedByOneTask * spawnedThreadNumber;
                //copying the values to be passed to task in local variables to avoid closure variable
                //issue. You can safely ignore this concept for now to avoid a detour. For now you
                //can assume I've done bad programming by creating two new local variables unnecessarily.
                var lowerLimit = lowerbound;
                var upperLimit = upperBound;
                
                tasks.Add(Task.Run(() => AddNumbersBetweenLimits(lowerLimit, upperLimit)));
                
            }

            Task.WhenAll(tasks).ContinueWith(task => CreateFinalSum(tasks));
            stopwatch.Stop();
            MessageBox.Show("time taken to do sum operation (in miliseconds) : " + stopwatch.ElapsedMilliseconds);
        }

        private static void CreateFinalSum(List<Task<long>> tasks)
        {
            var finalValue = tasks.Sum(task => task.Result);
            MessageBox.Show("Sum is : " + finalValue);
        }

        private static long AddNumbersBetweenLimits(long lowerLimitInclusive, long upperLimitInclusive)
        {
            long sumTotal = 0;
            for (long i = lowerLimitInclusive; i <= upperLimitInclusive; i++)
            {
                sumTotal += i;
            }

            return sumTotal;
        }

 

Let's evaluate the pit falls of conventional multi-threaded program w.r.t TPL based implementation. Do they still exist?

  1. Risky usage of global variables -> Not any more! A task can execute an asynchronous function with a non-void return type which was not supported earlier with normal threads or thread-pool threads. After the execution of work is complete you can query the task for the value returned from the executed function using <TaskInstance>.Result property. Since this was possible, so I implemented my algorithm in a way that I split the parllel work of part summation on individual tasks and asked them to stay ready with their partial sums when I query them in CreateFinalSum method. I didn't ask my individual tasks to go and update some global variable to create final sum as it would have again invited a contention for shared resource.
  2. Thread Synchronization -> Seamless thread synchronization: To know if all my individual 10 tasks have finished creating their partial sum I used Task.WhenAll(tasks).ContinueWith which is a very basic construct of task parallel library. When the lambda expression argument inside ContinueWith call executes I'm guaranteed with the fact that all my partial sums are ready. Now I call my CreateFinalSum method using the lambda expression which will simply add all the partial sums present in <TaskInstance>.Result in a sequential fashion without any fear of dirty read or wrong overwrite. References to all task instances were being maintained in a List data structure.
  3. Unresponsive UI -> Click the UI buttons whenever you like when your work is in progress on TPL tasks : No more hung up UIs and the "Not Responding" text in the title bar of your desktop application in TPL world. This is possibly the biggest relief your end users will see in your application if they are free to interact with UI even when the background tasks are in progress.  

                Firstly I want to be able to show you a hung up UI. Actually on modern hardware the summation  tasks we are doing in this exercise are still really really quick and we don't notice that UI got freezed for a second or anything like that. UI getting freezed/hung up is more prominent when you perform some long running task e.g. getting a data from a web service call or reading a huge file from disk. To keep my current example code simple I've simply added following statement to simulate a long running task.

    Thread.Sleep(5000); //sleeps the thread for 5 seconds to simulate long running task

                 You might want to uncomment the above line of code from AddNumbersWithSynchronization function to see the UI hung-up behavior. Now after uncommenting the above line of code and start the summation process again by clicking the button with caption "Multi-Threading With Synchronization", you will observe that you are unable to click any other button on the form. You can test this by clicking the button with caption "Click Me!" and see if a message box pops up. It will not.

                   Now for a similar reason I've added the same line of code in CreateFinalSum method as well so that it takes long time for TPL as well to finish the task. Now if you kick of the summation process again by clicking "Multi-Threading with Tasks in TPL" caption button, you will observe that it takes a while before your final sum is shown in a message box. But in the meantime your UI is completely responsive. You can click the "Click Me!" button n number of times to see a user friendly message. How does tasks achieve this? Let's take a bit of detour.

    Detour : Some Basics about Tasks and Task Parallel Library

    Now it is the time to learn a bit of basics and internals of tasks and their implementation which allows task to help your application exhibit a number of marvelous behaviour. Tasks are a world in their own and to get a full blown detail I would recommend reading chapters 26,27 and 28 from the book CLR via C# (4th Edition) by Jeffrey Richter. Well to understand them in most simple form - Tasks found  a place in .Net framework so that we can get rid of limitations of the existing APIs with the help of which we used to interact with Thread pool threads.

            A thread pool is a pool of threads managed by CLR for your process. The maximum count of Thread pool threads is a finite number. The maximum number of Thread Pool threads available to a process had been varying in every .Net release. Initially in .Net 1.1 it had started with 25. Today it stands at 1023 on a 32-bit machine and 32768 on a 64-bit machine. Every time you kick off your .Net executable the thread-pool threads are ready to execute any background task that you want to assign to them. There is an API ThreadPool.QueueUserWorkItem which was used to queue the work we wanted to execute on thread-pool threads. There were a number of shortcomings with this API.  To name a few issues, there was no way to know the completion or running status of threadpool thread which is executing your task. Also, there was no way of getting a return value from functions getting executed on ThreadPool threads and so on.

        Tasks and TPL came up in .Net v4.0 to give you this whole new model of working with Thread-Pool threads and remove all shortcomings in existing APIs to interact with thread pool. So the following line of code will simply get executed on a thread-pool thread. As a result your main UI thread remains absolutely free which gives you the opportunity to interact with the UI even when the summation process is in progress and you were able to click the button with caption "Click Me!"

    Task.WhenAll(tasks).ContinueWith(task => CreateFinalSum(tasks));

    There is whole new world of async functions in .Net 4 which help you achieve all this seamlessly using async-await keyword pairs. It is strongly recommended that you explore them here. In my opinion async function was the most fascinating feture of C# 4. For you quick information await keyword used inside async functions is the key to keep your UI responsive. await keyword lets even the waiting stuff (for a background work to finish) to happen on a thread-pool thread in place of keeping the main GUI thread busy. This is the real thing which allows your GUI to stay responsive. This will help you get rid of that red cloud in the picture I had shown your in an earlier section. I couldn't cover async-await keywords and async function in detail here given the increasing size of the article and to avoid more detours.
     
  4. Non cooperative cancellation -> Task do cooperate to end their life-cycle : Tasks support cooperative cancellation if you want to cancel them mid-way. It is very simple. In our application, click on the button with caption "Tasks with Cancellation" to kick off the summation process of first 1 million natural numbers. This time all the ten tasks that we invoke for getting partial sums support cancellation. Here is the signature of that method. Look at the third parameter of the function.


    private static long AddNumbersBetweenLimitsWithCancellation(long lowerLimitInclusive, long upperLimitInclusive, CancellationToken token)

    Every time before doing subsequent summation they check a flag IsCancellationRequested of the cancellation token that was passed initially to the function when it started. Now, considering this summation is a long running task you might want to cancel it mid-way. So click on the button with caption "Cancel Tasks". It results in IsCancellationRequested property of the token to be set to true.  This results in functions performing partial summation to immediately return instead of continuing any further.

    Effectively your function should also support graceful termination by using the token. Look how graceful it is. I simply ended my function instead of killing, murdering or aborting the thread which would have happened otherwise if it not were tasks. In the end instead of the grand total value, you get an error message as the system was able to detect that tasks were cancelled mid-way by the user. Here is the code snippet for the same.
C#
       //some code has been removed for brevity.
       //please refer attached source code for complete reference.
       CancellationTokenSource ts = new CancellationTokenSource();

        private void btnCancelTasks_Click(object sender, EventArgs e)
        {
            ts.Cancel();
        }

        private void btnTasksWithCancellation_Click(object sender, EventArgs e)
        {
            long degreeofParallelism = 10;
            long lowerbound = 0;
            long upperBound = 0;
            List<Task<long>> tasks = new List<Task<long>>();
            long countOfNumbersToBeAddedByOneTask = 100000; //1 lakh

            for (int spawnedThreadNumber = 1; spawnedThreadNumber <= degreeofParallelism; spawnedThreadNumber++)
            {
                lowerbound = ++upperBound;
                upperBound = countOfNumbersToBeAddedByOneTask * spawnedThreadNumber;
                //copying the values to be passed to task in local variables to avoid closure variable
                //issue. You can safely ignore this concept for now to avoid a detour. For now you
                //can assume I've done bad programming by creating two new local variables unnecessarily.
                var lowerLimit = lowerbound;
                var upperLimit = upperBound;

                tasks.Add(Task.Run(() => AddNumbersBetweenLimitsWithCancellation(lowerLimit, upperLimit, ts.Token)));

            }

            Task.WhenAll(tasks).ContinueWith(task => CreateFinalSumWithCancellationHandling(tasks));
        }


        private static long AddNumbersBetweenLimitsWithCancellation(long lowerLimitInclusive, long upperLimitInclusive, CancellationToken token)
        {
            long sumTotal = 0;
            for (long i = lowerLimitInclusive; i <= upperLimitInclusive; i++)
            {
                //deliberately added a sleep statement to emulate a long running task
                //this will give the user a chance to cancel the partial summation tasks in the middle when they are not yet complete.
                Thread.Sleep(1000);
                if (token.IsCancellationRequested)
                {
                    sumTotal = -1;//set some invalid value so that calling function can detect that method was cancelled mid way.
                    break;
                }
                sumTotal += i;
            }

            return sumTotal;
        }

        private static void CreateFinalSumWithCancellationHandling(List<Task<long>> tasks)
        {
            long grandTotal = 0;
            foreach (var task in tasks)
            {
                if (task.Result < 0)
                {
                    MessageBox.Show("Task was cancelled mid way. Sum opertion couldn't complete.");
                    return;
                }
                grandTotal += task.Result;
            }
            var finalValue = tasks.Sum(task => task.Result);
            //Did you require a context switch to UI worker thread here before showing the
            //MessageBox control which is a UI element. What would have you done if you were NOT using TPL.
            MessageBox.Show("Sum is : " + finalValue);
        }

 

We are gradually headings towards the end of the article. As much as I understand TPL as of today I 've tried bringing the fine prints where you can see how TPL gives you an edge over conventional multi-threaded world or the thread-pool threads. A few more interesting things are still remaining where tasks differ from conventional multi-threaded programming. Let's get going.

Updating UI controls from background threads

You might already be knowing what I'm going to talk about, if you have ever worked with background threads in windows form applications in the past. If not then let's do a vey small exercise. Go to AddNumbersWithoutThreadSynchronization function and uncomment the code lblTotal.Text = "2";Now click on button with caption "Multi-Threading without thread Synchronization". You will get InvalidOperationException as shown in the snapshot below. It says Cross-thread operation not valid.

Image 6

The basic concept behind this error is that all the UI controls are created and owned by main UI thread which starts when you hit the Main method of your program. The CLR poses a limitation that only the thread (UI thread) which had created those controls can modify the properties of UI controls e.g. if you want to change the Text, Size, Location properties of a label control then that should be performed from UI thread.

So when we are inside the method AddNumbersWithoutThreadSynchronization which is being executed on a non-UI thread (a thread pool thread which is a background thread by default), then this InvalidOperationException is getting thrown when I tried setting the Text porperty of lblTotal control.  How do we fix this? Not that difficult! If there is really a need of updating UI controls from some thread which is not a UI thread then employ the code I've done for the button with caption "Update UI Control from background". Here is the quick snippet for you for your ready reference:

 

C#
private void btnUpdateUiControl_Click(object sender, EventArgs e)
{
    ClearResultLabel();
    var threadStart = new ThreadStart(UpdateLabelTextAsync);
    var thread = new Thread(threadStart);
    thread.Start();
}

delegate void UpdateUi();

private void UpdateLabelTextAsync()
{
    UpdateUi functionUi = UpdateLabelControl;
    //do work which doesn't involve UI controls.
    //.....
    //.....
    //.....
    //.....
    //Now we have to update UI control so special handling required.
    //InvokeRequired == true means you are on a non-UI thread.
    if (lblTotal.InvokeRequired)
        lblTotal.BeginInvoke(functionUi);
}

private void UpdateLabelControl()
{
    lblTotal.Text = "Changed the text from background thread.";
}

 

So, basically you need to change your execution context from background thread to UI thread before updating the UI control. It is done through BeginInvoke method. To accomplish that we defined a delegate and a separate method UpdateLabelControl which does UI update related work.

It is possible to accomplish all this in TPL world also. Although it isn't transparent in TPL as well but you will find it a lot more cleaner. As I was talking about the main thread execution context in last paragraph under which you can update UI control. TPL achieves that logical execution context through the concept of task schedulers. There are two main types of task schedulers in TPL world which come with .Net framework, namely the thread-pool task scheduler (meant for executing tasks) and synchronization context task scheduler (meant for executing UI thread related work). If you can set the task scheduler being used currently to the later one while inside a task you are automatically switched to UI thread and you can make UI updates without raising an exception. If you click the button with caption "Update UI With Summation - TPL", you will see a label on the UI getting updated with sum of first 1 million natural numbers. This UI update happens from a function ContinuationAction getting executed asynchronously using a task. Let's see it working in code.

C#
        private readonly TaskScheduler uiContextTaskScheduler;

        #region Constructor and Finalizers
        public MainForm()
        {
            InitializeComponent();
            uiContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
        }
        #endregion

        private static long AddNumbersBetweenLimits(long lowerLimitInclusive, long upperLimitInclusive)
        {
            long sumTotal = 0;
            for (long i = lowerLimitInclusive; i <= upperLimitInclusive; i++)
            {
                sumTotal += i;
            }

            return sumTotal;
        }
       
        private void btnUpdateUiTpl_Click(object sender, EventArgs e)
        {
            ClearResultLabel();
            long degreeofParallelism = 10;
            long lowerbound = 0;
            long upperBound = 0;
            List<Task<long>> tasks = new List<Task<long>>();
            long countOfNumbersToBeAddedByOneTask = 100000; //1 lakh
            for (int spawnedThreadNumber = 1; spawnedThreadNumber <= degreeofParallelism; spawnedThreadNumber++)
            {
                lowerbound = ++upperBound;
                upperBound = countOfNumbersToBeAddedByOneTask * spawnedThreadNumber;
                //copying the values to be passed to task in local variables to avoid closure variable
                //issue. You can safely ignore this concept for now to avoid a detour. For now you
                //can assume that I've done a bad programming by creating two new local variables unnecessarily.
                var lowerLimit = lowerbound;
                var upperLimit = upperBound;

                tasks.Add(Task.Run(() => AddNumbersBetweenLimits(lowerLimit, upperLimit)));
            }
            Task.WhenAll(tasks).ContinueWith(ContinuationAction, tasks, uiContextTaskScheduler);
        }

        private void ContinuationAction(Task task, object o)
        {
            var partialSumTasks = (List<Task<long>>) o;
            var finalValue = partialSumTasks.Sum(eachtask => eachtask.Result);
            lblTotal.Text = "Sum is : " + finalValue;
        }

Let's disect the above piece of code a bit to understand what is happening over here. We knew it from beginning that all that matters is about getting the right context, and after that setting the Text property of a label control is a child's play as  you know it. So that UI context is passed to the ContinuationAction function through the third argument uiContextTaskScheduler in below line of code. Essentially, the whole of ContinuationAction will simply be executed on the main UI thread. So always try putting only that piece of code inside such functions whose only job is to update the UI. If you burden such function with more work which can be done on a background thread as well then your UI hung up issue will again start showing its head.

Task.WhenAll(tasks).ContinueWith(ContinuationAction, tasks, uiContextTaskScheduler);

Multi-Core CPUs - How TPL is better at leveraging them

You might have heard a lot of people saying this about TPL that TPL is better at leveraing multi-core CPUs which is quite a norm in the PC hardware world today. But, very basic thought process suggests that tasks are also ultimately some background threads doing my work which I can create myself then how comes does that make a difference in optimum utilization of multi-core CPU architecture. Let's quick skim through a few points which might get your grey cells working and will help you take TPL's route:

Creation and Maintainence of threads : Tasks ultimately leverage thread-pool threads which are optimally managed by CLR. If you implement the whole of multi-threading yourself then you bear the cost of creating, maintaining and disposing the threads which can always have issues. More threads created unnecessarily will simply increase your application's memory working size. TPL manages all these very efficiently.

Cost of Context-Switching: I already spoke about context-switching which happens when your CPU is dealing with giving CPU slice time to multiple threads existing in your system as per the OS scheduling algorithm. Context switching comes with a cost. More the number of threads, more will be the cost of context switching. Ideally for a 2 core CPU there should be 2 threads in parallel for best performance. So TPL is smart at various factors to be managed so that number of threads it is creating or disposing is optimal for your application and the system.

TPL has lot of heuristics built-in which makes an appropriate decision about creating a new thread-pool thread or utilizing an existing free thread from the pool itself or even killing the existing ones if they are sitting idle doing nothing. I've no doubts about your programming skills but building these heurists yourself as an individual programmer will be really complex and ultimately you will be simply reinventing the wheel which MS programmers have done it for you over the years. So why not leverage TPL?

Shall I use TPL for every multi-threading work? - No

Personally I believe TPL should be your de-facto choice for any new multi-threading work you are taking up but there are few pointers which will help you in deciding between the three worlds.

When to create and use threads manually?

If you seek absolute control of the new threads which are getting created in your system for the reasons which are mentioned below but are not limited to:

  1. When my thread should get created?
  2. How many new threads should get created in my process?
  3. When my thread should be disposed?
  4. What should be the priority of the new thread from OS scheduling algorithm stand-point? e.g. high, medium, low
  5. Should my thread be a background thread or a foreground thread?

When to use threadpool is more appropriate?

If your background workloads are primarily and purely fire-and-forget asynchronous work, then using QueueUserWorkItem may be appropriate e.g. Processing the content of csv files getting continuously created in a directory and then dumping the read content into a database table.

When using TPL is more appropriate?

Following scenarios might prompt you to use TPL:

  • If you want to use new features (continuation options, knowing the completion state of a thread, returning a value after work is done, etc) of TPL
  • If you want to keep your UI responsive all the time
  • If you have ever worked  or want to work on windows store applications then TPL is the new gateway to leverage thread-pool. Although, ThreadPool class has also been reintroduced in Windows.System.Threading namespace to leverage its QueueUserWorkItem API as Windows Store Apps don't have access to System.Threading namespace anymore.
  • If you have any other multi-threading need which doesn't fit other two scenarios mentioned in this section i.e. creating the thread directly for full control or using conventional thread pool for fire and forget scenario.

Points of Interest

  • How does Async-await keywords introduced in C# leverage TPL and tasks?

History

17-Jul-2016 - First Release

28-Jul-2016 - Added an example which is a good fit for leveraging conventional thread-pool threads.

10-Aug-2016 - Corrected the scenarios which are appropriate for TPL as Windows Store Apps also have access to ThreadPool class directly but through a different namespace.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)