Advance Threading and Tasks in C#

Discussion in 'C#' started by shabbir, Apr 16, 2014.

  1. shabbir

    shabbir Administrator Staff Member

    Joined:
    Jul 12, 2004
    Messages:
    15,375
    Likes Received:
    388
    Trophy Points:
    83
    In my last article, Complete Threading Tutorial in C#, I explained basics of threading. I explained that how threads can be created, how locking is implemented to make your applications thread-safe and how exception handling is implemented in threaded applications. This article explains some more advanced concepts. However, if you are not familiar with threads, I would advise you to first read my aforementioned article and then come to this article for advanced topics. Without wasting any further time, let us jump straight to the point.

    Foreground and Background Threads



    Threads which are created explicitly by the user are called foreground threads. The running time of the application is driven by the foreground threads and as long as any of these threads are executing, application keeps running. Background threads on the other hand have nothing to do with the runtime of an application. As soon as all the foreground threads complete their execution, the application ends immediately forcing all the background threads to terminate abruptly.

    You can check or change status of a thread by calling its IsBackground property. In our first example of this article, I will demonstrate the basic concepts of foreground and background threads in action.

    Example1
    Code:
    using System;
    using System.Threading;
    
    namespace CSharpTutorial
    {
        class Program
        {
            public static void Division(int num, int num2)
            {
                try
                {
                    int result = num / num2;
                    Console.WriteLine(result);
                    Console.ReadLine();
                }
                catch (Exception e)
                {
                    Console.WriteLine("Exception: " + e.Message);
                }
            }
            public static void Main()
            {
                Thread nt = new Thread(() => Division(10, 5));
                nt.Start();
                nt.IsBackground = true;
            }
        }
    }
    
    Here we have static method Division which takes two numbers, divides them ad displays the result on the console. And then it waits for the user to press any key. Now, come to the main method, here we have instantiated a Thread and named it ‘nt’. This thread will execute the Division method. We then called Start on this object. Now we have two threads, a Main thread and a new thread ‘nt’. Both of these threads are foreground threads and until both complete their execution the application will not end. The new thread will wait for the user to press any key because it contains a Console.ReadLine method after displaying the result.

    But what we did here is that we changed the IsBackground property of new thread ‘nt’ to true. This will cause new thread to run in background. Main thread will still be running on background. But this time as soon as the main thread completes execution, it will terminate the new thread as well since it is running in the background and you will see that this time the application will not wait for user to press a key because Console.ReadLine will now be executing on the background thread and will terminate as soon as the Main thread which is foreground, completes.

    Thread Signaling



    Thread signaling refers to a mechanism by which a thread waits and blocks its execution until it receives some signal from another thread. As soon as it receives signal from the other thread, it starts execution from the point from where it stopped execution. There are many signal construct that can be used to implement signaling mechanism. However, in our next example, we will use ManualResetEvent signal. An important thing to note here is that both the threads must share the signaling construct or in other words, signal construct should be static so that both the threads can access it.

    The thread which has to wait for signal, calls WaitOne on the signal instance. It blocks on that point unless it receives a signal from another thread which sends signal by calling Set method on the same signal instance. Our next example demonstrates this concept.

    Example2
    Code:
    using System;
    using System.Threading;
    
    namespace CSharpTutorial
    {
        class Program
        {
            static ManualResetEvent divisionsignal = new ManualResetEvent(false);
    
            public static void Division(int num, int num2)
            {
                try
                {
                    Console.WriteLine("Waiting on signal ...");
                    divisionsignal.WaitOne();
                    Console.WriteLine("Signal Received ...");
                    divisionsignal.Dispose();
                    
                    int result = num / num2;
                    Console.WriteLine(result);
                }
                catch (Exception e)
                {
                    Console.WriteLine("Exception: " + e.Message);
                }
            }
            public static void Main()
            {
                Thread nt = new Thread(() => Division(10, 5));
                nt.Start();
                Thread.Sleep(TimeSpan.FromSeconds(5));
                divisionsignal.Set();
                Console.ReadLine();
            }
        }
    }
    
    At the start of the Program class, we have a static ManualResetEvent signal which we named divisionsignal. Inside our Division method, we called WaitOne on this signal. Inside the main method we instantiated a thread nt and passed it delegate to Division method. Division method starts executing on a separate thread but it stops on WaitOne.

    Inside the Main method we called sleep of five second and after that we called Set method on the divisionsignal which will notify the new thread that you can now execute the Division method after WaitOne call. When you compile the code, you would see that Division method will wait for five seconds for signal to arrive before printing the division. The output of the code in Example2 is as follows:

    Output2

    [​IMG]

    You will see a gap of seconds between “Waiting on signal” and “Signal Received” being printed on screen, this is basically the time for which the new thread waits for signal.

    Thread Pool



    Thread instantiation involves some startup tasks such as allocating local stack memory for thread and creating thread space etc. It is not always suitable to create and instantiate a new thread via Thread class, for small operations that need to be run in parallel because startup overhead might consume more time than the operation that needs to be performed on the thread. For such small scale tasks, .NET Framework contains a set of already created threads which is known as the thread pool. Threads in the thread pool have three major characteristics:
    • Thread in a thread pool cannot be named.
    • These threads run always in the background.
    • Performance of the application is degraded substantially if pooled threads are blocked.
    In order to run any operation on a pooled thread, the easiest way is to use Task.Run method. This is shown in our next example. Note, for all the following examples, you should have .NET Framework 4.5 installed. The following example has been written in Visual Studio 2012 which automatically installs .NET framework 4.5.

    Example3
    Code:
    using System;
    using System.Threading.Tasks;
    
    namespace CSharpTutorial
    {
        class Program
        {
            static void Main(string[] args)
            {
                Task.Run(()=>Console.WriteLine("This thread belongs to threadpool"));
                Console.ReadLine();
            }
        }
    }
    
    Output3

    [​IMG]

    Task.Run method is integrated into .NET framework 4.5. For framework 4.0, you can use the following method:
    Code:
    Task.Factory.StartNew(()=> Console.WriteLine("This thread belongs to threadpool"));
    

    Tasks



    Threads are extremely efficient for implementing concurrency at low level in applications; however threads have few potential disadvantages. They are as follows:
    • You can pass data to a thread via a lambda expression call or by using start method but you cannot receive data from the thread that completes its execution.
    • When a thread completes its execution, you cannot instruct it to perform some other task.
    Also, large number of threads in an application leads to CPU oversubscription which is a phenomenon in which the number of threads exceeds the physical cores of the CPU. In that case, CPU divides its time into slices and periodically assigns this time to multiple threads following some scheduling algorithm.

    In contrary to threads, tasks are higher level abstraction. They may or may not make use of threads at lower level. Tasks can be chained by using continuations (we discuss them later) which make them compositional unlike threads. Tasks can make use of thread pools which help them reduce their startup time.

    Creating a Task



    Simplest way to create a Task is via Task.Run method which has already been explained in the Example3. In our next example, I shall explain the use of Wait method. The Wait in Task is equal to a Join in thread and blocks the thread from which it is called until the thread on which it is called completes execution. Have a look at Example4 to understand this concept.

    Example4
    Code:
    using System;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace CSharpTutorial
    {
        class Program
        {
            static void Main(string[] args)
            {
                Task newtask = Task.Run(() =>
                    {
                        Thread.Sleep(TimeSpan.FromSeconds(5));
                        Console.WriteLine("Task is being executed");
                    }
                );
    
                newtask.Wait();
                Thread.Sleep(TimeSpan.FromSeconds(2));
                Console.WriteLine("Executing main thread after waiting for task ...");
                Console.ReadLine();
            }
        }
    }
    
    In Example4, we have created a new task using, Task.Run method and passed it an Action delegate. In the Action delegate we specified that first wait for 5 seconds and then print a statement on console.

    Task.Run method returns a Task object which can be used to track the progress of the task while it is executing. We have newtask object of type Task, and we called Wait method on it. At this point of time, the Main thread blocks and waits for the Task to complete. The newtask completes its execution after 5 seconds of sleep and printing the statement. We then again wait for 2 seconds in the Main thread and then print a statement in the Main thread. The output of the code in Example4 is as follows:

    Output4

    [​IMG]

    Long Running Tasks



    If there are long running Tasks that block for a long time such as in Example3 where we blocked for 5 seconds in the Task, the performance of the application can suffer. If there is only one such long running task, it is okay to use thread from the thread pool by calling Task.Run, however, if there are multiple long running tasks, you should not use Task.Run because it creates pooled thread. To prevent creation of pooled thread, you can pass TaskCreationOptions.LongRunning as a second parameter to the old Task.Factory.StartNew method. It prevents use of pooled thread and creates long tasks. Our 5th example, explains this concept. Have a look at it:

    Example5
    Code:
    using System;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace CSharpTutorial
    {
        class Program
        {
            static void Main(string[] args)
            {
                Task newtask = Task.Factory.StartNew(() => 
                {
                    Thread.Sleep(TimeSpan.FromSeconds(5));
                    Console.WriteLine("This is not a pooled Thread ..."); 
                }, 
                    TaskCreationOptions.LongRunning
                );
                newtask.Wait();
      
                Console.WriteLine("Executing main thread after waiting for task ...");
                Console.ReadLine();
            }
        }
    }
    
    If you look at the code in Example5, you would see that now, we have passed a second parameter to the Task.Factory.StartNew method, after the Action delegate as shown below:
    Code:
                Task newtask = Task.Factory.StartNew(() => 
                {
                    Thread.Sleep(TimeSpan.FromSeconds(5));
                    Console.WriteLine("This is not a pooled Thread ..."); 
                }, 
                    TaskCreationOptions.LongRunning
                );
    
    The output of the code in Example5 is as follows:

    Output5

    [​IMG]

    Returning Values from Tasks



    We mentioned that we cannot return a value from thread, however we can achieve similar functionality by using a static variable in threads but that is not convenient and conventional way of returning values from threads. Tasks solve this issue for us. Task has a generic counterpart Task<ResultType>. Using this generic Task, you can get the value returned by the method that you pass as an Action delegate to the Task’s Run method. In our next example we are going to demonstrate this concept. Have a look at the 6th example of this tutorial.

    Example6
    Code:
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace CSharpTutorial
    {
        class Program
        {
            public static int Division(int num, int num2)
            {
                {
                    int result = num / num2;
                    return result;
                }
            }
    
            public static void Main()
            {
                Task<int>  nt = Task.Run(() => Division(10, 5));
                nt.Wait();
                Console.WriteLine("Returned Value by Division Task: "+ nt.Result);
                Thread.Sleep(TimeSpan.FromSeconds(5));
           
                Console.ReadLine(); 
            }
        }
    }
    
    In this example, the Division method returns an integer value. In the main method we have instantiated generic Task class with type integer and named it ‘nt’. Now, you can call Task.Run and pass it an Action delegate to Division method which will return integer value. The value returned by the division method can be accessed via the Result property of the generic Task object ‘nt’. We have then displayed this value on the screen. The output of the code in Example6 is as follows:

    Output6

    [​IMG]

    Exception Handling With Tasks



    Another major benefit of using Tasks over Thread is that exceptions that are unhandled in one Task are propagated back to the Tasks that are waiting for it or are accessing Result property of the task where unhandled exception occurs. This was not the case with the threads because threads did not propagate back the exceptions and in that case exception handling had to be implemented inside the function which was to run on a separate thread. Tasks, saves us from implementing exception handling for all those functions that run on tasks, and we can implement exception handling on Wait and Result property. In our next example, we are going to explain this concept. Have a look at the 7th example of this article.

    Example7
    Code:
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace CSharpTutorial
    {
        class Program
        {
            public static int Division(int num, int num2)
            {
                    int result = num / num2;
                    return result;
            }
    
            public static void Main()
            {
                try
                {
                    Task<int> nt = Task.Run(() => Division(10, 0));
                    nt.Wait();
                    Console.WriteLine("Returned Value by Division Task: " + nt.Result);
                    Thread.Sleep(TimeSpan.FromSeconds(5));
    
                    Console.ReadLine(); 
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.InnerException.Message);
                    Console.ReadLine(); 
                }
            }
        }
    }
    
    In Example7, we again have a method which we have named Division and this method divides two integers and returns the result. In our main method, we have instantiated a generic Task of type integer which we named ‘nt’. We called Rask.Run method and passed it Action delegate to Division method with parameters 10 and 0. Note that, all of this code is in Try and catch block in the Main method.

    When, Division method executes it tries to divide 10 by zero which results in DivideByZeroException. Visual studio code might break here because in debugger mode, code breaks for every unhandled exception, however you can press F10, to continue and you would see that on the console “Attempted to divide by zero” message appears.

    This is due to the reason that when DivideByZeroException occurs in the Division method, the Task propagates it back to the Main thread, from where this Task has been started. In the main thread when this exception reaches the Wait method or the Result property of its instantiated Task, again an outer exception occurs and if the Wait or the Resulted property of Task is enclosed in a Try block, the control is switched to the catch block.

    The catch block actually catches the outer exception and from this outer exception object, inner exception which is “DivideByZeroException” can be accessed and its message can be printed on screen as we have done in our 7th example. The output of the code in Example7 is as follows:

    Output7

    [​IMG]

    You can see that exception actually occurred on a separate Task, but it has been handled by the Main thread which was not the case with Threads. This is another advantage of using Tasks over Threads.

    Task Continuation



    In simplest words, Task continuation refers to the continuation of a Task to perform another activity once it has completed its execution. You tell the tasks that “hey, if you have completed your work, please do this one”. Continuation is actually implemented with the help of callbacks. When a task completes its execution, the callback is invoked which contains new instructions for the task.

    The callback is obtained by calling GetAwaiter in the Task instance which will return awaiter object. You can then call OnCompleted method on this awaiter object which gets invoked once the actual Task instance completes its execution. The OnCompleted method takes another Action delegate which actually refers to the continuation task that needs to be performed when primary task is completed by the Task instance. In our Example8 of this tutorial, I am going to explain the concept of continuation. Have a look at it:

    Example8
    Code:
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace CSharpTutorial
    {
        class Program
        {
            public static int Division(int num, int num2)
            {
                    int result = num / num2;
                    return result;
            }
    
            public static void Main()
            {
                    Task<int> nt = Task.Run(() => Division(15, 3));
    
                    var divisionawaiter = nt.GetAwaiter();
    
                    divisionawaiter.OnCompleted(() => 
                        {
                            int division = divisionawaiter.GetResult();
                            Console.WriteLine("Displaying division after awaiter completes: " + division);
                        });
                    Console.WriteLine("This will execute without awaiting ....");
                    Console.ReadLine(); 
            }
        }
    }
    
    Come to the Main method, here we have instantiated a generic Task and named it ‘nt’. This task actually takes an Action delegate to Division method which performs division. After calling Task.Run we got an awaiter object on ‘nt’ task object by calling GetAwaiter method. This awaiter object is stored in ‘divisionwaiter’ variable.

    Next line of code is extremely important, here you call OnCompleted method on divisionwaiter and passed it an Action delegate. This is basically continuation task. Once Task ‘nt’ finishes executing the Division method, this OnCompleted callback is invoked.

    In Action delegate inside the OnCompleted callback we first called GetResult on the divisionwaiter which stores the result of the actual Task, which was division in the local variable result. We then displayed this result. Note, that after this OnComplete callback in the main method, we printed a statement on the console. This statement might print earlier than the callback because Main thread and the newly created Task nt will run in parallel therefore callback will be called after the Division completes which will take some time. So, the statement in the Main thread will print first and then the message in the OnCompleted callback would be printed. The output of the code in Example8 is as follows:

    Output8

    [​IMG]

    You can see that the first line of the output is actually the statement which is written after the OnCompleted callback in the main method but it has been printed first. And the statement in the OnCompleted callback was written first but printed later, the reason is that the callback is basically a continuation of the Task and waits for the actual Task to complete whereas the statement in the Main method doesn’t wait for anyone.

    Concurrency is one of the most fascinating features of modern day programming and in .NET this is done via threads and tasks. This article discusses both the concepts in detail. I would suggest you to further explore System.Threading and System.Threading.Tasks namespace to see what other important types are that are used for concurrency and what their functionalities are. For more interesting tutorials on threading, keep visiting this site.
     
    Last edited: Jan 21, 2017

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice