Advance Threading and Tasks in C#
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.
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.
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 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.
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:
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 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:
Task.Run method is integrated into .NET framework 4.5. For framework 4.0, you can use the following method:
Threads are extremely efficient for implementing concurrency at low level in applications; however threads have few potential disadvantages. They are as follows:
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.
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.
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:
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:
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.
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.
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:
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.
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:
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:
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.
|All times are GMT +5.5. The time now is 15:41.|