Friday, December 7, 2007

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;
 

No comments: