Advanced CLR Topics
- Threading in .NET
- Exception Handling in .NET
- The MSIL Disassembler
- From Here
In this chapter
- Threading in .NET
- Exception Handling in .NET
- The MSIL Disassembler
- From Here
Visual Studio .NET introduces the Common Language Runtime (CLR). As discussed in Chapter 16, "The Common Language Runtime," the CLR establishes an environment that allows the developer to create applications that can be used in a hosted, managed code space. This allows the developer to create portable, secure applications that will run on any system that hosts the CLR. Two topics are important to constructing applications in the CLR and bear further inspection: Threading in .NET and Exception Handling in .NET. We will also be looking at ILDASM, a tool that comes with Visual Studio .NET and helps the developer to understand managed code applications.
Threading in .NET
Threading for native code C++ applications was introduced in Chapter 14, "Multitasking with Windows Threads". Using threads in the .NET Framework is similar in concept to using them in C++ applications, but different in implementation. The .NET Framework uses the classes contained in the System::Threading namespace to help the developer create and manage threads. In this section we will focus on the differences of creating, using and destroying threads in a .NET application.
Overview of Threads in Managed Code
When using threads in a .NET application, all of the functions necessary to work with the threads are contained in the System::Threading namespace. The System::Threading namespace implements the Thread class whose members are similar in function to the Win32 API that C++ uses in the native code implementation of threads. Table 25.1 shows the relationship of some of the methods in System::Threading to the corresponding functions in the Win32 API.
Table 25.1 Threading Functions in .NET and Win32
System::Threading |
Win32 API |
Thread Creation: |
|
Thread |
CreateThread |
ThreadStart |
|
Thread Destruction: |
|
Thread.Abort |
TerminateThread |
Thread Management: |
|
Thread.Priority |
SetThreadPriority |
Thread.Join |
WaitForSingleObject |
Thread.Sleep |
Sleep |
Thread.Suspend |
SuspendThread |
Thread.Resume |
ResumeThread |
NOTE
There are some Win32 functions (like ExitThread) and System::Threading classes (like Thread.Name and Thread.IsBackground) that do not have equivalent functions or classes in the other corresponding implementation of threading.
CAUTION
When using this table, be sure you understand the side effects of a particular function or class. They may not map directly from one to the other and would require additional changes to your application to work properly with your design.
For example, Thread.Abort and TerminateThread do not exactly match each other in functionality. TerminateThread will remove a thread from execution and does not perform any cleanup of the thread's resources nor is it preventable. However, Thread.Abort follows the conventions of managed code: it will clean up the thread resources and can be prevented if the thread implements ResetAbort.
Creating a Thread in Managed Code
Creating a thread using the Common Language Runtime is simple. The Thread class has methods necessary to create, manage and destroy threads. To illustrate the ease of creating a thread in an application, this chapter builds a simple console based Managed C++ application that creates and uses a single thread.
First, create a Managed C++ project. Start Visual Studio .NET and select File, New, Project to bring up the New Project dialog box as shown in Figure 25.1.
Figure 25.1 The New Project dialog box.
In the Project Types area on the left side of the dialog box, select Visual C++ Projects. Select a Managed C++ Application on the right hand side and enter ThreadEx as the name of the project. When you have done all this and the dialog looks similar to Figure 25.2, then click OK to create the project.
Figure 25.2 Completed New Project dialog box for the ThreadEx sample application.
After the project is created, open the ThreadEx.cpp file using the Solution Explorer. This file is the .cpp file created for your application. When it is opened you will see the basic code skeleton for a Managed C++ console application.
In order to use the threading classes without typing their full name each time, add the System::Threading namespace to the application. Add the following code after the using statement for the System namespace:
using namespace System::Threading;
Adding this line of code allows the application to refer to the classes and methods defined in the System::Threading namespace with their shorter names, such as Thread.
A thread executes a function, and in Managed C++ it's a member function of a class. Enter the following class definition after the declaration for the System::Threading namespace.
public __gc class SimpleThreadClass { public: // The method that will be called when the thread is started. void ThreadCounterProc() { Console::WriteLine(S"Worker thread invoked, will count to 10."); for(int x=1; x!=11; x++) Console::WriteLine(x); Console::WriteLine(S"Worker thread is terminating."); } };
The declaration of the class is simple enough except for the new keyword __gc. This keyword is part of the Managed extensions for C++ that have been added to the language to support the Common Language Runtime. The __gc keyword denotes that the class will be managed by the Common Language Runtime and that it is garbage collected. This means that a specific call to destroy the class is not necessary and that the class will be cleaned up by the Common Language Runtime at the end of its lifetime. This is discussed in more detail in Chapter 20, "Managed and Unmanaged Code."
The rest of the class defines the member function, ThreadCounterProc()that is the core of the processing that the thread will do. The method is defined as a public method and does not have a return value. It begins by writing to the console that the thread has begun and then proceeds to count to 10. Each iteration of the for loop is written to the console so that the user can see this happening. Finally, the notice that the thread is terminating is displayed and then the thread will cease execution.
Next, add the code to the _tmain function to create the thread object, invoke the thread and complete the processing of the worker thread and the main application. Delete the two lines provided in the original project in the _tmain function between the function declaration and the return statement. Then add the following code to the application.
Console::WriteLine(S"Main application thread is starting."); SimpleThreadClass *objSimpleThread = new SimpleThreadClass(); //Create the thread object Thread *objWorkerThread = new Thread(new ThreadStart(objSimpleThread, &SimpleThreadClass::ThreadCounterProc)); //Start the thread objWorkerThread->Start(); Console::WriteLine(S"Main application thread is terminating.");
The _tmain function defines the main thread of execution for the application. Did I mention that an application always contains at least one thread? The worker thread created for the new Thread object will execute independently of the main application thread but lives in the same process space as the main application thread. Only when all threads of an application are terminated will the process close and the operating system clean up the resources of the process.
The first line of the application writes to the console window that the application's main thread is beginning execution. This is followed by a declaration and initialization of a pointer to a SimpleThreadClass. Now we come to the meat of the thread creation and invocation. It declares a Thread object pointer called objWorkerThread that will be used to manipulate the thread. In the same statement it constructs the objWorkerThread.
Let's take a closer look at that statement:
new Thread(new ThreadStart(objSimpleThread, &SimpleThreadClass::ThreadCounterProc));The constructor's basic form uses a delegate function (ThreadStart) with two parameters. A delegate is the Common Language Runtime version of the function pointer in C++. The first parameter is the pointer to the thread class object. The second is the address of the ThreadCounterProc in the SimpleThreadClass object. This delegate function constructor then returns the necessary information into the Thread constructor so that when the thread starts, the Thread object knows what method of the SimpleThreadClass to invoke. The same initialization code could also have been written as:
ThreadStart *MyThreadStart = new ThreadStart(objSimpleThread, &SimpleThreadClass::ThreadCounterProc); Thread *objWorkerThread = new Thread(MyThreadStart);
This shows more clearly the relationship of the two method calls.
The next statement invokes the thread and begins its executionby calling Start, a member function objWorkerThread inherited from the Thread class. Then the main thread of execution continues, writes to the console that it is complete, and terminates.
The new thread executes parallel to the main thread of execution does its work by executing the function.After entering the code into the ThreadEx.cpp file, build it and run it.
The program output may at first surprise you. Here is an example of the output:
Main application thread is starting. Main application thread is terminating. Worker thread invoked, will count to 10. 1 2 3 4 5 6 7 8 9 10 Worker thread is terminating. Press any key to continue
Notice that the main application thread starts and ends before any worker thread output is produced. This is because the main thread of execution does not wait until the worker thread is complete before terminating. The worker thread's processing is in parallel to the main thread and it does not matter if the main thread is terminated[md]the worker thread will continue to do its job. You are unlikely to want this behavior in a real application because, most likely, the worker thread is doing some processing that the main thread will want later. Therefore, it would not be wise to terminate the main thread before the worker thread is done with its processing. These are the same threading issues developers tackled in C++ in the unmanaged world, and you can use the same types of mechanisms to solve the problem in the managed world.
CAUTION
Starting a thread is not like making a function call. Do not expect the thread call to stop the execution of the main thread while the worker thread is doing its work. The worker thread is executing in parallel to the main thread and operates independently for the most part.
Threads are an excellent way to take separate tasks that may need to run concurrently and have the computer work on them all at the same time. However, threads are usually not a good choice when one item needs to be completed before another can take place.
Using threads in Managed C++ is very simple and it is easy to add additional threads by creating additional worker thread objects and starting them as in the preceeding example. Sometimes it is necessary to place priority of one thread over another. How to do that is the subject of the next section.
Setting Thread Priority
When you are working with many threads, it may be necessary to change the thread's priority in the application. There are several different levels of priority that can be used in scheduling threads. However, like many things with multitasking, the thread priority does not tell the compiler in which order the threads are to be executed. It provides a clue to the Common Language Runtime what the relative importance of a thread compared with others that the application is running. Therefore, when setting thread priority, the question to ask yourself is not "What thread do I want to run first?" but "What thread is more important to process relative to the others that the application has?"
The priority of a thread is stored in the Priority property of the Thread object. This value can be changed by the application in order to make sure that a thread gets scheduling priority over another. You set the thread priority after the Thread object is instantiated but before the thread begins execution. Table 25.2 shows the values of the ThreadPriority enum that is set in the Priority property.
Table 25.2 ThreadPriority Values
Value |
Description |
Highest |
Thread will be scheduled before all other priorities. |
Above Normal |
Thread will be scheduled between threads of Highest and Normal priorities. |
Normal (Default) |
Thread will be scheduled between threads of Above Normal and Below Normal priorities. |
Below Normal |
Thread will be scheduled between threads of Normal and Lowest priorities. |
Lowest |
Thread will be scheduled after all other priorities. |
Using the ThreadPriority is simple. Create another solution called ThreadPriEx[md]again a Managed C++ application. To the ThreadPriEx.cpp file, add the code from Listing 25.1.
Listing 25.1 The ThreadPriEx.cpp Sample
#include "stdafx.h" #using <mscorlib.dll> #include <tchar.h> using namespace System; using namespace System::Threading; public __gc class SimpleThreadClass1 { public: // The method that will be called when the thread is started. void ThreadCounterProc() { Console::WriteLine(S"Worker thread #1 started, will count to 10."); for(int x=1; x!=11; x++) Console::WriteLine(x); Console::WriteLine(S"Worker thread #1 is terminating."); } }; public __gc class SimpleThreadClass2 { public: // The method that will be called when the thread is started. void ThreadCounterProc() { Console::WriteLine(S"Worker thread #2 started, will count to 10."); for(int x=1; x!=11; x++) Console::WriteLine(x); Console::WriteLine(S"Worker thread #2 is terminating."); } }; public __gc class SimpleThreadClass3 { public: // The method that will be called when the thread is started. void ThreadCounterProc() { Console::WriteLine(S"Worker thread #3 started, will count to 10."); for(int x=1; x!=11; x++) Console::WriteLine(x); Console::WriteLine(S"Worker thread #3 is terminating."); } }; // This is the entry point for this application int _tmain(void) { Console::WriteLine(S"Main application thread is starting."); SimpleThreadClass1 *objSimpleThread1 = new SimpleThreadClass1(); SimpleThreadClass2 *objSimpleThread2 = new SimpleThreadClass2(); SimpleThreadClass3 *objSimpleThread3 = new SimpleThreadClass3(); //Create the thread objects Thread *objWorkerThread1 = new Thread(new ThreadStart(objSimpleThread1, &SimpleThreadClass1::ThreadCounterProc)); Thread *objWorkerThread2 = new Thread(new ThreadStart(objSimpleThread2, &SimpleThreadClass2::ThreadCounterProc)); Thread *objWorkerThread3 = new Thread(new ThreadStart(objSimpleThread3, &SimpleThreadClass3::ThreadCounterProc)); objWorkerThread3->Priority=ThreadPriority::Highest; objWorkerThread2->Priority=ThreadPriority::Normal; objWorkerThread1->Priority=ThreadPriority::Lowest; //Start the thread objWorkerThread1->Start(); objWorkerThread2->Start(); objWorkerThread3->Start(); Console::WriteLine(S"Main application thread is terminating."); return 0; }
This code is very similar to the ThreadEx sample discussed earlier, but instead of one SimpleThreadClass there are now three that are similar in design. The creation of the Thread object is also very similar. The big difference is where we make the thread priority assignments:
objWorkerThread3->Priority=ThreadPriority::Highest; objWorkerThread2->Priority=ThreadPriority::Normal; objWorkerThread1->Priority=ThreadPriority::Lowest;
To change the schedule priority of the thread we make a call to the ThreadPriority member function of each Thread object and assign it to the Priority property. We then start each thread and then conclude the execution of the main thread.
After you build the solution, execute it and you should see the following:
Main application thread is starting. Main application thread is terminating. Worker thread #3 started, will count to 10. 1 2 3 4 5 6 7 8 9 10 Worker thread #3 is terminating. Worker thread #2 started, will count to 10. 1 2 3 4 5 6 7 8 9 10 Worker thread #2 is terminating. Worker thread #1 started, will count to 10. 1 2 3 4 5 6 7 8 9 10 Worker thread #1 is terminating. Press any key to continue
So even though WorkerThread1 was started first WorkerThread3 was the first to complete its work. This is because the priority of the WorkerThread3 was set higher than WorkerThread1.
NOTE
Thread priority is a suggestion to the operation system on how to allocate processing time and priority of scheduling to a thread. Higher priority threads are scheduled to run first and receive more processing time than lower priority threads. But since this is only a suggestion, if the Common Runtime Language deems it necessary to lower the priority of a thread temporarily then it has the ability to do so. Normally, when a thread priority is lowered in this manner then it will be scheduled more as expected later in execution if possible.
CAUTION
While it is often very useful to schedule threads to higher or lower priorities, do not do so unless the task that the thread is running really requires the change in priority. Normal priority is fine for most purposes. If you schedule too many Highest priority threads, it is possible to starve other threads and processes for CPU time. This would have the net effect of making the whole application appear slower when judicial use of thread priority could actually make the application run faster.
Destroying Threads
Sometimes it is necessary to terminate the execution of a thread. Before managed code, this was not an easy task and was not recommended because of the issues with interruption of execution and housekeeping. Now with garbage collection in managed code it is much easier.
You have already used the most common method of thread destruction, which is to let the thread terminate normally when the thread is done executing. The thread object and all thread specific allocations are cleaned up by the Common Runtime Language. However, sometimes it is necessary to terminate a thread before it finishes normally. The Common Language Runtime Thread class has a method that you can use to terminate a thread. It is Thread::Abort.
Thread::Abort allows for a smooth termination of the thread. Managed code does not support a method of thread termination that is similar to the Win32 API TerminateThread(). TerminateThread would stop the execution of a thread dead in its tracks, however, that is not how objects should behave in the CLR. When you use Thread::Abort, the thread will stop when it has reached a point where garbage collection can take place. Only then will it stop execution.
As an example, here is what the ThreadEx sample could look like if we decided to terminate the thread execution right away.
int _tmain(void) { Console::WriteLine(S"Main application thread is starting."); SimpleThreadClass *objSimpleThread = new SimpleThreadClass(); //Create the thread object Thread *objWorkerThread = new Thread(new ThreadStart(objSimpleThread, &SimpleThreadClass::ThreadCounterProc)); //Start the thread objWorkerThread->Start(); //Oops, we really don't want that thread to run after all Console::WriteLine(S"Main application is terminating thread."); objWorkerThread->Abort(); Console::WriteLine(S"Main application thread is terminating."); return 0; }
When the call to Thread::Abort is made, the Common Language Runtime will throw a ThreadAbortException. This exception cannot be caught by a managed code application, but the Common Language Runtime will execute any __finally clauses that it finds when it winds the stack, thus allowing you to perform any cleanup that is necessary for your application (not necessarily by the thread object or its local properties or members).
You will not be notified when the thread is terminated but if it is necessary for you to know, you can use Thread::Join to tell you when the thread has been destroyed.
Thread Pooling
While you can create, schedule and destroy threads in managed code using the Thread class, the recommended way to use threads in managed code is something else entirely, called a ThreadPool.
ThreadPools have been around for a while but have not been very easy to use. Now with the Common Language Runtime, ThreadPool is far easier to use than the Thread class. There is no thread object management that the application must do and each application gets its own ThreadPool object to play with. So what exactly is a ThreadPool and why did we just spend the past while talking about the Thread object?
A typical application will not use threads very often and only for short term tasks. After the task is complete, the thread goes into a sleeping state and waits to be used again. This uses resources and requires management on the part of the application. Wouldn't it be nice if there was a way that you could call a thread only when you needed it and then when it was done you could forget about it? Well, that is a ThreadPool. You call ThreadPool::QueueUserWorkItem and pass it a WaitCallback delegate function then when the system is ready, it will create a thread, assign the thread to do the work of the function that you listed as the callback with QueueUserWorkItem and then clean up the thread when it is done.
Threads that are created from a ThreadPool are not like threads created from Thread. The most important difference is that they are by default a background thread and not a foreground thread. In the managed code world, a background thread will only execute as long as the main thread of execution is still running. So, if you terminate the main thread before the ThreadPool thread is done, then it will be terminated as well. Normally, this is not much of an issue but it is something to be aware of. Also, You cannot set the priority of a ThreadPool thread, and all ThreadPool threads are run in the COM multithreaded apartment model. This may seem like a lot of limitations but remember that the main purpose of a ThreadPool thread is to handle asynchronous types of tasks. These are usually things that need to be done without being tracked.
Here's an example similar to the ThreadEx sample that uses a ThreadPool object thread instead of a Thread object thread. Create a Managed C++ application as before and name it ThreadPoolEx. Enter the code in Listing 25.2.
Listing 25.2 The ThreadPoolEx Sample
#include "stdafx.h" #using <mscorlib.dll> #include <tchar.h> using namespace System; using namespace System::Threading; public __gc class SimpleThreadClass { public: // The method that will be called when the thread is started. static void ThreadCounterProc(Object *) { Console::WriteLine(S"Worker thread invoked, will count to 10."); for(int x=1; x!=11; x++) Console::WriteLine(x); Console::WriteLine(S"Worker thread is terminating."); } }; // This is the entry point for this application int _tmain(void) { Console::WriteLine(S"Main application thread is starting."); SimpleThreadClass *objSimpleThread = new SimpleThreadClass(); //Start the thread ThreadPool::QueueUserWorkItem(new WaitCallback(objSimpleThread, &SimpleThreadClass::ThreadCounterProc)); Thread::Sleep(1000); Console::WriteLine(S"Main application thread is terminating."); return 0; }
You will notice that the code is slightly shorter than the code for ThreadEx. Here is how it works. First, the SimpleThreadClass is basically the same. There is one important difference and that is the declaration of the ThreadCounterProc. Now it takes a parameter of Object * and is a static void function. This is important because all functions that are registered with the WaitCallback method have to match that method's declaration. In other words, all threads that use a ThreadPool will be constructed in a similar fashion. You need not pass an object pointer but it is there if you need to.
The line that does all the work for the queuing of the thread is:
ThreadPool::QueueUserWorkItem(new WaitCallback(objSimpleThread, &SimpleThreadClass::ThreadCounterProc));
A call to the ThreadPool method QueueWorkItem includes the construction of a callback function -- the function of our thread. This is done by calling WaitCallback with the pointer to our thread object and the entry point for the thread to begin execution.After that, just sit back and wait for execution to begin.
Now, because the thread is run in the background, it is necessary to have the main thread sleep for some time to allow the worker thread to execute, which is why there is a call to Thread::Sleep afterwards. In a normal application, you could use other methods to signal the main thread when the execution is done so that it would not terminate too soon.
ThreadPools are certainly easy to use and will most likely be the first choice for most applications. It is lightweight for your application, managed by the Common Runtime Language and suitable to most purposes. However, you also have the flexibility to use the Thread object when you need to have tighter control of the threads in your application.