FREE BOOK

Chapter 10: Processes, AppDomains,Contexts, and Threads

Posted by Apress Free Book | C#.NET February 02, 2009
In this Chapter you drill deeper into the constitution of a .NET executable host and come to understand the relationship between Win32 processes, application domains, contexts, and threads.

Gathering Basic Thread Statistics

Recall that the entry point of an executable assembly (i.e., the Main() method) runs on the primary thread of execution. To illustrate the basic use of the Thread type, assume you have a new console application named ThreadStats. The static Thread.CurrentThread property retrieves a Thread type that represents the currently executing thread. Thus, if you trigger this member within the scope of Main(), you are able to print out various statistics about the primary thread:

          using System.Threading;

       static void Main(string[] args)
        {
            // Get some info about the current thread.
            Thread primaryThread = Thread.CurrentThread;
            // Get name of current AppDomain and context ID.
            Console.WriteLine("***** Primary Thread stats *****");
            Console.WriteLine("Name of current AppDomain: {0}",
            Thread.GetDomain().FriendlyName);
            Console.WriteLine("ID of current Context: {0}",
            Thread.CurrentContext.ContextID);
            Console.WriteLine("Thread Name: {0}", primaryThread.Name);
            Console.WriteLine("Apt state: {0}", primaryThread.ApartmentState);
            Console.WriteLine("Alive: {0}", primaryThread.IsAlive);
            Console.WriteLine("Priority Level: {0}", primaryThread.Priority);
            Console.WriteLine("Thread State: {0}", primaryThread.ThreadState);
            Console.WriteLine();
        }

Naming Threads

When you run this application, notice how the name of the default thread is currently an empty string. Under .NET, it is possible to assign a human-readable string to a thread using the Name property. Thus, if you wish to be able to programmatically identify the primary thread via the moniker "ThePrimaryThread," you could write the following:

              // Name the thread.
            primaryThread.Name = "ThePrimaryThread";
            Console.WriteLine("This thread is called: {0}", primaryThread.Name);

.NET Threads and Legacy COM Apartments

Also notice that every Thread type has a property named ApartmentState. Those of you who come from a background in classic COM may already be aware of apartment boundaries. In a nutshell, COM apartments were a unit of isolation used to group COM objects with similar threading needs. Under the .NET platform however, apartments are no longer used by managed objects. If you happen to be making use of COM objects from managed code (via the interoperability layer), you are able to establish the apartment settings that should be simulated to handle the coclass in question. To do so, the .NET base class libraries provide two attributes to mimic the single-threaded apartment (STAThreadAttribute, which is added by default to your application's Main() method when using the VS .NET IDE) and multithreaded-apartment (MTAThreadAttribute) of classic COM. In fact, when you create a new *.exe .NET application type, the primary thread is automatically established to function as an STA:

          // This attribute controls how the primary thread should
        // handle COM types.
        [STAThread]
        static void Main(string[] args)
        {
            // COM objects will be placed into an STA.
        }

If you wish to specify support for the MTA, simply adjust the attribute:

       [MTAThread]
        static void Main(string[] args)
        {
            // COM objects will be placed into the MTA.
        }

Of course, if you don't know (or care) about classic COM objects, you can simply leave the [STAThread] attribute on your Main() method. Doing so will keep any COM types thread-safe without further work on your part. If you don't make use of COM types within the Main() method, the [STAThread] attribute does nothing.

Setting a Thread's Priority Level

As mentioned, we programmers have little control over when the thread scheduler switches between threads. We can, however, mark a given thread with a priority level to offer a hint to the CLR regarding the importance of the thread's activity. By default, all threads have a priority level of normal. However this can be changed at any point in the thread's lifetime using the ThreadPriority property and the related ThreadPriority enumeration:

public enum System.Threading.ThreadPriority
{
AboveNormal, BelowNormal,
Highest, Lowest,
Normal, // Default value.
}

Always keep in mind that a thread with the value of ThreadPriority.Highest is not necessarily guaranteed to given the highest precedence. Again, if the thread scheduler is preoccupied with a given task (e.g., synchronizing an object, switching threads, moving threads, or whatnot) the priority level will most likely be altered accordingly. However, all things being equal, the CLR will read these values and instruct the thread scheduler how to best allocate time slices. All things still being equal, threads with an identical thread priority should each receive the same amount of time to perform their work.

NOTE Again, you will seldom (if ever) need to directly alter a thread's priority level. In theory, it is possible to jack up the priority level on a set of threads, thereby preventing lower priority threads from executing at their required levels (so use caution).

Spawning Secondary Threads

When you wish to create additional threads to carry on some unit of work, you need to interact with the Thread class as well as a special threading-related delegate named ThreadStart. The general process is quite simple. First and foremost, you need to create a function (static or instance-level) to perform the additional work. For example, assume the current SimpleThreadApp project defines the following additional static method, which mimics the work seen in Main():

        static void MyThreadProc()
        {
            Console.WriteLine("***** Secondary Thread stats *****");
            Thread.CurrentThread.Name = "TheSecondaryThread";
            Thread secondaryThread = Thread.CurrentThread;
            Console.WriteLine("Name? {0}", secondaryThread.Name);
            Console.WriteLine("Apt state? {0}", secondaryThread.ApartmentState);
            Console.WriteLine("Alive? {0}", secondaryThread.IsAlive);
            Console.WriteLine("Priority? {0}", secondaryThread.Priority);
            Console.WriteLine("State? {0}", secondaryThread.ThreadState);
            Console.WriteLine();
        }

NOTE The target for the ThreadStart delegate cannot take any arguments and must return void.

Now, within Main(), create a new Thread class and specify a new ThreadStart delegate as a constructor parameter (note the lack of parentheses in the constructor when you give the method name). To inform the CLR that this new thread is ready to run, call the Start() method (but always remember that Start() doesn't actually start the thread). Starting a thread is a nondeterministic operation under the total control of the CLR- you can't do anything to force the CLR to execute your thread. It will do so on its own time and on its own terms:

       [STAThread]
        static void Main(string[] args)
        {
            // Start a secondary thread.
            Thread secondaryThread = new Thread(new ThreadStart(MyThreadProc));
            secondaryThread.Start();
        }

The output is seen in Figure 10-11.



Figure 10-11. Your first multithreaded application

One question that may be on your mind is exactly when a thread terminates. By default, a thread terminates as soon as the function used to create it in the ThreadStart delegate has exited.

Foreground Threads and Background Threads

The CLR assigns a given thread to one of two broad categories:

  • Foreground threads: Foreground threads have the ability to prevent the current application from terminating. The CLR will not shut down an application (which is to say, unload the hosting AppDomain) until all foreground threads have ended.
  • Background threads: Background threads (sometimes called daemon threads) are viewed by the CLR as expendable paths of execution, which can be ignored at any point in time (even if it is currently laboring over some unit of work). Thus, if all foreground threads have terminated, any and all background threads are automatically killed.

It is important to note that foreground and background threads are not synonymous with primary and worker threads. By default, every thread you create via the Thread.Start() method is automatically a foreground thread. Again, this means that the AppDomain will not unload until all threads of execution have completed their units of work. In most cases, this is exactly the behavior you require.

For the sake of argument, however, assume that you wish to spawn a secondary thread that should behave as a background thread. Again, this means that the method pointed to by the Thread type (via the ThreadStart delegate) should be able to halt safely as soon as all foreground threads are done with their work. Configuring such a thread is as simple as setting the IsBackground property to true:

           // Start a new background thread.
            Thread secondaryThread = new Thread(new ThreadStart(MyThreadProc));
            secondaryThread.Priority = ThreadPriority.Highest;
            secondaryThread.IsBackground = true;


Now, to illustrate the distinction, assume that the MyThreadProc() method has been updated to print out 1000 lines to the console, pausing for 5 milliseconds between iterations using the Thread.Sleep() method (more on the Thread.Sleep() method later in this chapter):

        static void MyThreadProc()
        {
            for (int i = 0; i < 1000; i++)
            {
                Console.WriteLine("Value of i is: {0}", i);
                Thread.Sleep(5);
            }
        }

If you run the application again, you will find that the for loop is only able to print out a tiny fraction of the values, given that the secondary Thread object has been configured as a background thread. Given that the Main() method has spawned a primary foreground thread, as soon as the secondary thread has been started, it is ready for termination.

Now, you are most likely to simply allow all threads used by a given application to remain configured as foreground threads. If this is the case, all threads must finish their work before the AppDomain is unloaded from the hosting process. Nevertheless, marking a thread as a background type can be helpful when the worker-thread in question is performing noncritical tasks or helper tasks that are no longer needed when the main task of the program is over.

Total Pages : 13 89101112

comments