Complete Threading Tutorial (C#)
Often times, applications need to perform multiple tasks at a time. I always share Microsoft’s word processor’s example. While you type in the word processor, the application lets you type and in the meantime it is running spell-check and dictionary in the background to inform you whenever you type wrong spelling of a word. Basically multiple tasks are being done in a single program. This is the principle concurrency, to take advantage of multiprocessor hardware and to run multiple application parts simultaneously. Concurrency results in a more responsive application. Concurrency in .NET is implemented via threads. For now, you just need to keep in mind that tasks that take too much time should be run in a separate thread so that application can perform other important tasks meanwhile. Let us start with the most fundamental concept of concurrency i.e Threading.
As usual, .NET Framework comes with a set of classes that help us create thread right away. These classes are located in System.Threading namespace. In case of a client application such as a console application or a windows forms application, a static thread is automatically created by operating system which is responsible for kick-starting the application. This thread is the “Main” thread which starts when the Main method starts execution. Inside this Main method, you can start as many threads as you want. In our first example we are going to demonstrate this concept. To see how threads are actually created, have a look at our first example.
When you call Start, two things would happen. A new thread starts execution and the control shifts to the next line in the Main thread. Which means that now, DisplayZero method is being executed in parallel with the Main method. In the Main method, we displayed one, thousand times. Now, DisplayZero thread is printing zeros and Main thread is printing ones, simultaneously. Both of these threads are sharing the console which would shift between two threads in an irregular manner. When you look at the output you would see irregular patterns of ones and zeros. The output of the code in Example1 is as follows:
When a thread is running, its IsAlive property returns true. A thread stops when the execution of the delegate passed to its constructor ends. When a thread has terminated, it cannot be restarted ever again.
Join and Sleep are two of the extremely important methods of the Thread class. When you call join on a thread object. The thread from which join is called waits for the thread to complete on which join is called. Sleep method pauses the thread in which it is called for a time period specified in its parameters. Our next example explains the concept of Join and Sleep methods. Have a look at Example2 of this article.
Now, come towards the DisplayZero method that would run in zero thread created in the Main method. Inside the DisplayZero method, we called static Sleep method of the Thread class and pass it a time span of one second. Now if you build and run the above code, you would see that first all the zeros would be displayed after a delay of one second and then ones would be printed. The delay is because of sleep and printing of all zeros is because of Join method which would block the execution of the for loop that displays ones in the Main thread, until zero thread completes. The output of the code in Example2 is as follows:
You would see that zeros are printed after one second because there is a Sleep of one second in zero’s thread while ones would be printed immediately since there is no Sleep in the Main method that prints one.
An important point to note here is that when a thread goes to blocking state via a Join or Sleep, it relinquishes its CPU slice of time and CPU can process other threads mean while until the sleep time is over or a thread is again invoked at the end of Join. CPU performs context switching when a thread goes to blocking or unblocking state.
In operating system’s terms, operations can be of two types. IO bound operations are those who wait for something to open. These operations are implemented via threads that wait for something to happen such as downloading of a webpage, waiting for the user to enter something etc. CPU bound operations are those operations who spend most of their time performing CPU intensive tasks.
Blocking vs. Spinning
Another important concept is that of Spinning. Blocking occurs when a thread waits for something to happen. Spinning occurs when a thread executes continuously in the form of a loop waiting continuously for something to happen.
Local vs. Shared Thread data
If several threads are sharing a process’s isolated environment, they have some shared data and some common data. Local variables are exclusive to a thread and stored on individual thread’s local variable stack. They are not shared by all the threads in the process. Our next example demonstrates this concept.
You can see that the numbers are printed in random order because both of the threads are running in parallel.
The real strength of the threads lay in their ability to share data among themselves. Data can be shared amongst threads in two ways. If two more threads have common reference to the instance of an object, the data of the object is shared amongst the threads. The other way of sharing data between multiple threads is via static fields. We will demonstrate the second method in our next example. Have a look at the 4th example of this program.
Like, in the previous method, inside the Main function we created an anonymous thread and passed it the DisplayNum method as a delegate. We start the thread in the same line. Similarly, we directly called the DisplayNum method from the Main method as well. Now, we have two methods simultaneously calling DisplayNum method.
The real magic begins here, the thread that reaches the condition ‘if (!completed)’ first, will find that bool variable ‘completed’ is false hence it will make this variable true and will display the value. Now, since this ‘completed’ variable is static it is being shared between the Main thread and the new anonymous thread. The thread that reaches the condition ‘if (!completed)’ later would find that ‘completed’ variable is true, therefore it would not execute further. If you see the output of the code in Example4, you would see only single bool value.
There is a potential problem with data sharing between threads. In Example4, we do not know whether the Main thread or the new anonymous thread that is responsible for printing the output. It depends upon the thread which got CPU cycles earlier and we do not know the internal mechanism by which CPU cycles are assigned to the tread. So, we do not know that which thread actually executed first.
If you look at the code in Example4 closely, you would find that both the threads can display the value of the bool variable ‘completed’. For instance if both the threads have checked the if condition but none of them have changed the value of the bool variable ‘completed’ to true, both threads would change it to true and would display its value. (This is rare but possible). We can say that the variable ‘completed’ is not thread safe and some locking mechanism has to be introduced between them. This is what we do in this section.
.NET framework provides lock statement to which gives exclusive lock to a thread which reaches that statement first and is authorized to execute that statement. Think of lock as a key, when a user wants to enter a room it obtains a key from the administration and while user is in the room no one can enter it. When user leaves the room it gives back the key to the administration and next person in the queue can obtain the key and enter a room. To implement a lock, you need a reference type object followed by a block of code on which this lock is implemented. Our next example modifies the code in Example4. Have a look at our 5th example to understand locks.
In all of our previous examples, a new thread is being used to execute a method that doesn’t take any parameters. However, this is not the case with real life programming scenarios where you need to pass data to threads or in other words you want to execute methods with parameters inside threads. But till now, we have been passing delegates with no parameters in the Thread class’s constructor. To pass, data to a thread or to pass parameters to a method that runs in a separate thread, we have two options. We can use lambda expression to initialize a method with parameters inside the constructor of Thread class. Secondly, we can pass parameters to the Start method of the Thread class instance. In our next example, we shall see the lambda expression technique for passing data to threads.
Lambda expressions were introduced in .NET framework 3.0, so for the legacy system you might need to adapt the old technique of passing data. Our next example demonstrates this technique. Have a look at the 7th example of this tutorial.
Now come inside the main method, in the constructor of the Thread class you passed a parameter-less method DisplayNumber but when you called the Start method, you passed an integer value to Start method which is actually the parameter to the DisplayNumber method. We passed, 7 which mean that first seven numbers starting from 0 would be displayed in the output.
There are two potential drawbacks of this second technique. First is that you can pass only one argument using Start method and secondly since the method which you want to run in thread can have only one parameter of type object, you need to perform explicit casting to use that parameter inside the method.
Since threads have their own independent path of execution, therefore they cannot be handled in ordinary way by enclosing Thread instantiation in try catch block. You must specify exception handling mechanism inside the method which is due to be run on a thread. Our last example of this tutorial demonstrates this concept. Have a look at the 8th example.
|All times are GMT +5.5. The time now is 19:55.|