Friday, December 7, 2007

Defining Threads

Threading Defined

By the end of this section, you will understand the following:

  • What multitasking is and what the different types of multitasking are

  • What a process is

  • What a thread is

  • What a primary thread is

  • What a secondary thread is

Multitasking

As you probably know, the term multitasking refers to an operating system's ability to run more than one application at a time. For instance, while this chapter is being written, Microsoft Outlook is open as well as two Microsoft Word windows, with the system tray showing further applications running in the background. When clicking back and forth between applications, it would appear that all of them are executing at the same time. The word "application" is a little vague here, though; what we really are referring to are processes. We will define the word "process" a little more clearly later in this chapter.

Classically speaking, multitasking actually exists in two different flavors. These days Windows uses only one style in threading, which we will discuss at length in this book. However, we will also look at the previous type of multitasking so we can understand the differences and advantages of the current method.

In earlier versions of Windows - such as Windows 3.x - and in some other operating systems, a program is allowed to execute until it cooperates by releasing its use of the processor to the other applications that are running. Because it is up to the application to cooperate with all other running programs, this type of multitasking is called cooperative multitasking. The downside to this type of multitasking is that if one program does not release execution, the other applications will be locked up. What is actually happening is that the running application hangs and the other applications are waiting in line. This is quite like a line at a bank. A teller takes one customer at a time. The customer more than likely will not move from the teller window until all their transactions are complete. Once finished, the teller can take the next person in line. It doesn't really matter how much time each person is going to spend at the window. Even if one person only wants to deposit a check, they must wait until the person in front of them who has five transactions has finished.

Thankfully, we shouldn't encounter this problem with current versions of Windows (2000 and XP) as the method of multitasking used is very different. An application is now allowed to execute for a short period before it is involuntarily interrupted by the operating system and another application is allowed to execute. This interrupted style of multitasking is called pre-emptive multitasking. Pre-emption is simply defined as interrupting an application to allow another application to execute. It's important to note that an application may not have finished its task, but the operating system is going to allow another application to have its time on the processor. The bank teller example above does not fit here. In the real world, this would be like the bank teller pausing one customer in the middle of their transaction to allow another customer to start working on their business. This doesn't mean that the next customer would finish their transaction either. The teller could continue to interrupt one customer after another - eventually resuming with the first customer. This is very much like how the human brain deals with social interaction and various other tasks. While pre-emption solves the problem of the processor becoming locked, it does have its own share of problems as well. As you know, some applications may share resources such as database connections and files. What happens if two applications are accessing the same resource at the same time? One program may change the data, then be interrupted, allowing another program to again change the data. Now two applications have changed the same data. Both applications assumed that they had exclusive access to the data.

Processes

When an application is launched, memory and any other resource for that application are allocated. The physical separation of this memory and resources is called a process. Of course, the application may launch more than one process. It's important to note that the words "application" and "process" are not synonymous. The memory allocated to the process is isolated from that of other processes and only that process is allowed to access it.

In Windows, you can see the currently running processes by accessing the Windows Task Manager. Right-clicking in an empty space in the taskbar and selecting Task Manager will load it up, and it will contain three tabs: Applications, Processes, and Performance. The Processes tab shows the name of the process, the process ID (PID), CPU usage, the processor time used by the process so far, and the amount of memory it is using. Applications and the processes appear on separate tabs, for a good reason. Applications may have one or more processes involved. Each process has its own separation of data, execution code, and system resources

Threads

You will also notice that the Task Manager has summary information about process CPU utilization. This is because the process also has an execution sequence that is used by the computer's processor. This execution sequence is known as a thread. This thread is defined by the registers in use on the CPU, the stack used by the thread, and a container that keeps track of the thread's current state. The container mentioned in the last sentence is known as Thread Local Storage. The concepts of registers and stacks should be familiar to any of you used to dealing with low-level issues like memory allocation; however, all you need to know here is that a stack in the .NET Framework is an area of memory that can be used for fast access and either stores value types, or pointers to objects, method arguments, and other data that is local to each method call.

Single-Threaded Processes

As noted above, each process has at least one of these sequential execution orders, or threads. Creating a process includes starting the process running at a point in the instructions. This initial thread is known as the primary or main thread. The thread's actual execution sequence is determined by what you code in your application's methods. For instance, in a simple .NET Windows Forms application, the primary thread is started in the static Main () method placed in your project. It begins with a call to Application.Run().

Multithreaded Processes

As you probably already know, we can split up our process to share the time slice allotted to it. This happens by spawning additional threads of execution within the process. You may spawn an additional thread in order to do some background work, such as accessing a network or querying a database. Because these secondary threads are usually created to do some work, they are commonly known as worker threads. These threads share the process's memory space that is isolated from all the other processes on the system. The concept of spawning new threads within the same process is known as free threading.

The concept of free threading gives a significant advantage over the apartment-threading model - the threading model used in Visual Basic 6.0. With apartment threading, each process was granted its own copy of the global data needed to execute. Each thread spawned was spawned within its own process, so that threads could not share data in the process's memory. Let's look at these models side by side for comparison.

Thread Support in .NET and C#

Free threading is supported in the .NET Framework and is therefore available in all .NET languages, including C# and VB.NET. we will look at how that support is provided and more of how threading is done as opposed to what it is. We will also cover some of the additional support provided to help further separate processes

By the end of this section, you will understand:

  • What the System.AppDomain class is and what it can do for you

  • How the .NET runtime monitors threads

System.AppDomain

When we explained processes earlier in this chapter, we established that they are a physical isolation of the memory and resources needed to maintain themselves. We later mentioned that a process has at least one thread. When Microsoft designed the .NET Framework, it added one more layer of isolation called an application domain or AppDomain. This application domain is not a physical isolation as a process is; it is a further logical isolation within the process. Since more than one application domain can exist within a single process, we receive some major advantages. In general, it is impossible for standard processes to access each other's data without using a proxy. Using a proxy incurs major overheads and coding can be complex. However, with the introduction of the application domain concept, we can now launch several applications within the same process. The same isolation provided by a process is also available with the application domain. Threads can execute across application domains without the overhead associated with inter-process communication. Another benefit of these additional in-process boundaries is that they provide type checking of the data they contain.

Microsoft encapsulated all of the functionality for these application domains into a class called System.AppDomain. Microsoft .NET assemblies have a very tight relationship with these application domains. Any time that an assembly is loaded in an application, it is loaded into an AppDomain. Unless otherwise specified, the assembly is loaded into the calling code's AppDomain. Application domains also have a direct relationship with threads; they can hold one or many threads, just like a process. However, the difference is that an application domain may be created within the process and without a new thread

Threading Principles

Multiple Threads in Applications

If you programmed in versions of VB prior to .NET, you might know that VB supported multiple threads within a COM container, such as MTS or COM+. Well, although multiple threads were supported by VB5/6, the threading model they supported was Single Threaded Apartments (STA). If you come from Visual C++ then you'd have options to build both MTA (Multi Threaded Apartments) and STA applications. However, the .NET Framework does not retain the concept of Apartments and it manages all of the threads within AppDomains. By default, all .NET applications are multithreaded and any code can access any object at any time. Thus, we have to be very careful with static resources in the managed code.

The .NET Framework supports both managed and unmanaged threads and all the Win32 threading models such as STA and MTA. When you are trying to access COM components from managed code, unmanaged threads are created by the legacy COM components. Threads in the .NET Framework are created using the Thread object, whether managed or unmanaged.

If you have ever programmed multithreaded programs using the Win32 APIs, you may remember that Win32 supported user-interface threads and worker threads. the threading names have now changed into Apartment Model Threading and Free Threading respectively. The .NET Framework supports two basic threading models, which are Apartment Model Threaded or Single Threaded Apartment (STA) components, and Free Threaded or Multi Threaded Apartment (MTA) components. When we create a thread in .NET, by default it is an MTA thread.

Scaling Threaded Applications

The CLR and Threads

The CLR was designed with the aim of creating a managed code environment offering various services such as compilation, garbage collection, memory management, and, as we'll see, thread pooling to applications targeted at the .NET platform.

Indeed, there is a remarkable difference between how Win32 and the .NET Framework define a process that hosts the threads that our applications use. In a traditional multithreaded Win32 application, each process is made up of collections of threads. Each thread in turn consists of Thread Local Storage (TLS) and Call Stacks for providing time slices in the case of machines that have a single CPU. Single processor machines allot time slices for each thread to execute based on the thread priority. When the time slice for a particular thread is exhausted, it is suspended and some other thread is allowed to perform its task. In the case of the .NET Framework, each Win32 process can be sub-divided logically into what are known as Application Domains that host the threads along with the TLS and call stack. It's worthwhile to note that communication between application domains is handled by a concept called Remoting in the .NET Framework.

Having gained a basic understanding on concepts of thread pooling and the .NET process, let's dig into how the CLR provides us with thread pooling functionality for .NET applications.

The Role of the CLR in Thread Pooling

The CLR forms the heart and soul of the .NET Framework offering several services to managed applications, thread pooling being one of them. For each task queued in the thread pool (work items), the CLR assigns a thread from the pool (a worker thread) and then releases the thread back to the pool once the task is done.

Thread pools are always implemented by the CLR using a multithreaded apartment (MTA) model by employing high performance queues and dispatchers through preemptive multitasking. This is a process in which CPU time is split into several time slices. In each time slice, a particular thread executes while other threads wait. Once the time slice is exhausted, other threads are allowed to use the CPU based on the highest priority of the remaining threads. The client requests are queued in the task queue and each item in this queue is dispatched to the first available thread in the thread pool.

Once the thread completes its assigned task, it returns to the pool and waits for the next assignment from the CLR. The thread pool can be fixed or of dynamic size. In the former case, the number of threads doesn't change during the lifetime of the pool. Normally, this type of pool is used when we are sure of the amount of resources available to our application, so that a fixed number of threads can be created at the time of pool initialization. This would be the case when we are developing solutions for an intranet or even in applications where we can tightly define the system requirements of the target platform. Dynamic pool sizes are employed when we don't know the amount of resources available, as in the case of a web server that will not know the number of client requests it will be asked to handle simultaneously.

Caveats to Thread Pooling

There is no doubt that thread pooling offers us a lot of advantages when building multithreaded applications, but there are some situations where we should avoid its use. The following list indicates the drawbacks and situations where we should avoid using thread pooling:

  • The CLR assigns the threads from the thread pool to the tasks and releases them to the pool once the task is completed. There is no direct way to cancel a task once it has been added to the queue.

  • Thread pooling is an effective solution for situations where tasks are short lived, as in the case of a web server satisfying the client requests for a particular file. A thread pool should not be used for extensive or long tasks.

  • Thread pooling is a technique to employ threads in a cost-efficient manner, where cost efficiency is defined in terms of quantity and startup overhead. Care should be exercised to determine the utilization of threads in the pool. The size of the thread pool should be fixed accordingly.

  • All the threads in the thread pool are in multithreaded apartments. If we want to place our threads in single-thread apartments then a thread pool is not the way to go.

  • If we need to identify the thread and perform various operations, such as starting it, suspending it, and aborting it, then thread pooling is not the way of doing it.

  • Also, it is not possible to set priorities for tasks employing thread pooling.

  • There can be only one thread pool associated with any given Application Domain.

  • If the task assigned to a thread in the thread pool becomes locked, then the thread is never released back to the pool for reuse. These kinds of situations can be avoided by employing effective programmatic skills.

The Size of the Thread Pool

The .NET Framework provides the ThreadPool class located in the System.Threading namespace for using thread pools in our applications. The number of tasks that can be queued into a thread pool is limited by the amount of memory in your machine. Likewise, the number of threads that can be active in a process is limited by the number of CPUs in your machine. That is because, as we already know, each processor can only actively execute one thread at a time. By default, each thread in the thread pool uses the default task and runs at default priority in a multithreaded apartment. The word default seems to be used rather vaguely here. That is no accident. Each system can have default priorities set differently. If, at any time, one of the threads is idle then the thread pool will induce worker threads to keep all processors busy. If all the threads in the pool are busy and work is pending in the queue then it will spawn new threads to complete the pending work. However, the number of threads created can't exceed the maximum number specified. By default, 25 thread pool threads can be created per processor. However, this number can be changed by editing the CorSetMaxThreads member defined in mscoree.h file. In the case of additional thread requirements, the requests are queued until some thread finishes its assigned task and returns to the pool. The .NET Framework uses thread pools for asynchronous calls, establishing socket connections, and registered wait operations.

Debugging and Tracing Threads

Creating the Application Code

Usually, when you create an application (or part of one), you write the code and then try to run the application. Sometimes it works as you expected it to; often it doesn't. When it doesn't, you try to discover what could be happening by examining more carefully the code that you wrote. In Visual Studio .NET, you can use the debugger by choosing some breakpoints to stop the application's execution near or just before the guilty method, then step through lines of code, examining variable values to understand precisely what went wrong. Finally, when all is working you can build the release version (a version without the symbols used by the debugging tools) of our application, and distribute it.

In this type of program, during the development process, you can also insert tracing functionality. In fact, even if the program works very well, there will be always a case where something has not been foreseen (especially when some external, possibly third-party, components fail). In that case, if you have filled the code with tracing instructions, you can turn on tracing and examine the resulting log file to understand what might have happened. Moreover, tracing functionality is useful to discover where an application consumes resources or where it spends too much time performing a task. In applications that use threads, you should use tracing functionality because otherwise it can be difficult to observe each thread's behavior, identify race conditions, and spot potential deadlocks or time-consuming contention.

Tracing, debugging, and performance techniques are often known as instrumentation. This term refers to the capacity to monitor an application's performance and behavior, and diagnose errors and bugs. So, an application that supports instrumentation has to include:

  • Debugging: Fixing errors and bugs during an application's development

  • Code tracing: Receiving information in a listener program during an application's execution

  • Performance counters: Using techniques to monitor an application's performance


Stepping Through the Code

Now that we have briefly described the more useful debugger windows, we can focus our attention on code navigation. The Visual Studio .NET debugger allows developers to step between code lines of both single and multiple source code files, observing the program behavior at run time. Moreover, you can debug unmanaged code and Microsoft SQL Server stored procedures. The debugger provides three different ways to step through the code:

  • Step Into: Pressing the F11 key you will go through the code one step at a time, entering method bodies that you find on your way (where source code and debug symbols are available).

  • Step Over: Pressing the F10 key you will go one step forward in the code executing every method you encounter but without stepping into it (executing the method as one line).

  • Step Out: Pressing Shift+F11, you will execute all the remaining code within the body of the method that you are currently stepped into, and step onto the next line in the method that called it.

Each time you step to the next line of code by pressing these keys, you are executing the highlighted code.

Another useful feature provided by the Visual Studio .NET debugger is the Run To Cursor functionality. Selecting it from the context menu over the source code, you can execute all the lines between the highlighted line and the line where the cursor is placed.

Finally, the Visual Studio .NET debugger provides a way to change the execution point of our application. You can decide to move your application's execution point by launching the debugger and choosing the Set Next Statement item in the context menu. Be careful when using this feature, because every line of code between the old and the new position will fail to be executed.

Code Tracing

The next code instrumentation technique that we will analyze is tracing. In a multi-threaded application, this technique is especially important. You can trace a thread's behavior and interaction when more than one task has been started. As we will see later, this is not possible using the debugger. The .NET Framework provides some useful classes that allow developers to implement tracing functionality simply. Let's examine the tracing classes from the System.Diagnostics namespace that the .NET Framework offers:

  • Trace: This class has many static methods that write messages to a listener. By default, the debug output windows in VS.NET will be used as the listener application, but thanks to the Listeners collection, you can add different listeners such as a text file listener, or the Windows event log listener.

  • Debug: This class has the same methods as the Trace class, writing information to a listener application. The largest difference between these two classes is in their usage; Trace is useful at run time, Debug is used at development time.

  • BooleanSwitch: This class allows us to define a switch that turns on or off the tracing messages.

  • TraceSwitch: This class provides four different tracing levels allowing developers to choose the severity of the messages to send to the listener.

The Trace Class

In this section, we will analyze the most frequently used methods of the Trace class. It is provided by the .NET Framework and encapsulates all the necessary methods to implement tracing functionality easily. The Trace class is contained in the System.Diagnostics namespace and provides many static methods for sending messages to the listener application. As you know, static methods mean that you do not have to instantiate a new object from the Trace class and can use the method directly. For example:

   static void Main()
{
Trace.WriteLine(t.ThreadState);
}

The Code

Let's start analyzing the code of the DataImport example:

   using System;
using System.IO;
using System.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Diagnostics;

namespace DataImport1
{
class DataImport
{

First of all, we referenced all the necessary namespaces to use the FileSystemWatcher, Thread, and SQL Server classes:

   public static BooleanSwitch bs;

[STAThread]
static void Main(string[] args)
{
// Remove the default listener
Trace.Listeners.RemoveAt(0);

// Create and add the new listener
bs = new BooleanSwitch("DISwitch", "DataImport switch");
Trace.Listeners.Add(new TextWriterTraceListener(
new FileStream(@"C:\DataImport.log", FileMode.OpenOrCreate)));

Then the code removes the default listener and creates a new TextWriterTraceListener object that points to C:\DataImport.log:

   // Create a FileSystemWatcher object used to monitor
// the specified directory
FileSystemWatcher fsw = new FileSystemWatcher();

// Set the path to watch and specify the file
// extension to monitor for
fsw.Path = @"C:\temp";
fsw.Filter = "*.xml";

// No need to go into subdirs
fsw.IncludeSubdirectories = false;

// Add the handler to manage the raised event
// when a new file is created
fsw.Created += new FileSystemEventHandler(OnFileCreated);
// Enable the object to raise the event
fsw.EnableRaisingEvents = true;