[ Team LiB ] Previous Section Next Section

20.1 Threads

Threads are typically created when you want a program to do two things at once. For example, assume you are calculating pi (3.141592653589...) to the 10 billionth place. The processor will happily begin computing this, but nothing will write to the user interface while it is working. Because computing pi to the 10 billionth place will take a few million years, you might like the processor to provide an update as it goes. In addition, you might want to provide a Stop button so that the user can cancel the operation at any time. To allow the program to handle the click on the Stop button, you will need a second thread of execution.

An apartment is a logical container within a process, and is used for objects that share the same thread-access requirements. Objects in an apartment can all receive method calls from any object in any thread in the apartment. The .NET Framework does not use apartments, and managed objects (objects created within the CLR) are responsible for thread safety. The only exception to this is when managed code talks to COM. COM interoperability is discussed in Chapter 22.

Another common place to use threading is when you must wait for an event, such as user input, a read from a file, or receipt of data over the network. Freeing the processor to turn its attention to another task while you wait (such as computing another 10,000 values of pi) is a good idea, and it makes your program appear to run more quickly.

On the flip side, note that in some circumstances, threading can actually slow you down. Assume that in addition to calculating pi, you also want to calculate the Fibonnacci series (1,1,2,3,5,8,13,21...). If you have a multiprocessor machine, this will run faster if each computation is in its own thread. If you have a single-processor machine (as most users do), computing these values in multiple threads will certainly run slower than computing one and then the other in a single thread, because the processor must switch back and forth between the two threads. This incurs some overhead.

20.1.1 Starting Threads

The simplest way to create a thread is to create a new instance of the Thread class. The Thread constructor takes a single argument: a delegate type. The CLR provides the ThreadStart delegate class specifically for this purpose, which points to a method you designate. This allows you to construct a thread and to say to it, "When you start, run this method." The ThreadStart delegate declaration is:

public delegate void ThreadStart( );

As you can see, the method you attach to this delegate must take no parameters and must return void. Thus, you might create a new thread like this:

Thread myThread = new Thread( new ThreadStart(myFunc) );

myFunc must be a method that takes no parameters and returns void.

For example, you might create two worker threads, one that counts up from zero:

public void Incrementer( )
{
    for (int i =0;i<10;i++)
    {
        Console.WriteLine("Incrementer: {0}", i);
    }
}

and one that counts down from 10:

public void Decrementer( )
{
    for (int i = 10;i>=0;i--)
    {
        Console.WriteLine("Decrementer: {0}", i);
    }
}

To run these in threads, create two new threads, each initialized with a ThreadStart delegate. These in turn would be initialized to the respective member functions:

Thread t1 = new Thread( new ThreadStart(Incrementer) );
Thread t2 = new Thread( new ThreadStart(Decrementer) );

Instantiating these threads does not start them running. To do so you must call the Start method on the Thread object itself:

t1.Start( );
t2.Start( );

If you don't take further action, the thread will stop when the function returns. You'll see how to stop a thread before the function ends later in this chapter.

Example 20-1 is the full program and its output. You will need to add a using statement for System.Threading to make the compiler aware of the Thread class. Notice the output, where you can see the processor switching from t1 to t2.

Example 20-1. Using threads
namespace Programming_CSharp
{
   using System;
   using System.Threading;

   class Tester
   {
      static void Main( )
      {
         // make an instance of this class
         Tester t = new Tester( );

         // run outside static Main
         t.DoTest( );                 
            
      }

      public void DoTest( )
      {
         // create a thread for the Incrementer
         // pass in a ThreadStart delegate
         // with the address of Incrementer
         Thread t1 = 
            new Thread( 
               new ThreadStart(Incrementer) );

         // create a thread for the Decrementer
         // pass in a ThreadStart delegate
         // with the address of Decrementer
         Thread t2 = 
            new Thread( 
               new ThreadStart(Decrementer) );

         // start the threads
         t1.Start( );
         t2.Start( );
      }

      // demo function, counts up to 1K
      public void Incrementer( )
      {
         for (int i =0;i<1000;i++)
         {
            Console.WriteLine(
               "Incrementer: {0}", i);
         }
      }

      // demo function, counts down from 1k
      public void Decrementer( )
      {
         for (int i = 1000;i>=0;i--)
         {
            Console.WriteLine(
               "Decrementer: {0}", i);
         }
      }
   }
}

Output:
Incrementer: 102
Incrementer: 103
Incrementer: 104
Incrementer: 105
Incrementer: 106
Decrementer: 1000
Decrementer: 999
Decrementer: 998
Decrementer: 997

The processor allows the first thread to run long enough to count up to 106. Then, the second thread kicks in, counting down from 1000 for a while. Then the first thread is allowed to run. When I run this with larger numbers, I notice that each thread is allowed to run for about 100 numbers before switching. The actual amount of time devoted to any given thread is handled by the thread scheduler and will depend on many factors, such as the processor speed, demands on the processor from other programs, and so forth.

20.1.2 Joining Threads

When you tell a thread to stop processing and wait until a second thread completes its work, you are said to be joining the first thread to the second. It is as if you tied the tip of the first thread on to the tail of the second—hence "joining" them.

To join thread 1 (t1) onto thread 2 (t2), write:

t2.Join( );

If this statement is executed in a method in thread t1, t1 will halt and wait until t2 completes and exits. For example, we might ask the thread in which Main( ) executes to wait for all our other threads to end before it writes its concluding message. In this next code snippet, assume you've created a collection of threads named myThreads. Iterate over the collection, joining the current thread to each thread in the collection in turn:

foreach (Thread myThread in myThreads)
{
    myThread.Join( );
}

Console.WriteLine("All my threads are done.");

The final message All my threads are done will not be printed until all the threads have ended. In a production environment, you might start up a series of threads to accomplish some task (e.g., printing, updating the display, etc.) and not want to continue the main thread of execution until the worker threads are completed.

20.1.3 Suspending Threads

At times, you want to suspend your thread for a short while. You might, for example, like your clock thread to suspend for about a second in between testing the system time. This lets you display the new time about once a second without devoting hundreds of millions of machine cycles to the effort.

The Thread class offers a public static method, Sleep, for just this purpose. The method is overloaded; one version takes an int, the other a timeSpan object. Each represents the number of milliseconds you want the thread suspended for, expressed either as an int (e.g., 2000 = 2000 milliseconds or 2 seconds) or as a timeSpan.

Although timeSpan objects can measure ticks (100 nanoseconds), the Sleep( ) method's granularity is in milliseconds (1,000,000 nanoseconds).

To cause your thread to sleep for one second, you can invoke the static method of Thread. Sleep, which suspends the thread in which it is invoked:

Thread.Sleep(1000);

At times, you'll tell your thread to sleep for only one millisecond. You might do this to signal to the thread scheduler that you'd like your thread to yield to another thread, even if the thread scheduler might otherwise give your thread a bit more time.

If you modify Example 20-1 to add a Thread.Sleep(1) statement after each WriteLine( ), the output changes significantly:

for (int i =0;i<1000;i++)
{
   Console.WriteLine(
      "Incrementer: {0}", i);
   Thread.Sleep(1);
}

This small change is sufficient to give each thread an opportunity to run once the other thread prints one value. The output reflects this change:

Incrementer: 0
Incrementer: 1
Decrementer: 1000
Incrementer: 2
Decrementer: 999
Incrementer: 3
Decrementer: 998
Incrementer: 4
Decrementer: 997
Incrementer: 5
Decrementer: 996
Incrementer: 6
Decrementer: 995

20.1.4 Killing Threads

Typically, threads die after running their course. You can, however, ask a thread to kill itself by calling its Abort( ) method. This causes a ThreadAbortException exception to be thrown, which the thread can catch, and thus provides the thread with an opportunity to clean up any resources it might have allocated.

catch (ThreadAbortException)
{
    Console.WriteLine("[{0}] Aborted! Cleaning up...",
        Thread.CurrentThread.Name);
}

The thread ought to treat the ThreadAbortException exception as a signal that it is time to exit, and as quickly as possible. You don't so much kill a thread as politely request that it commit suicide.

You might wish to kill a thread in reaction to an event, such as the user pressing the Cancel button. The event handler for the Cancel button might be in thread t1, and the event it is canceling might be in thread t2. In your event handler, you can call Abort on t1:

t1.Abort( );

An exception will be raised in t1's currently running method that t1 can catch. This gives t1 the opportunity to free its resources and then exit gracefully.

In Example 20-2, three threads are created and stored in an array of Thread objects. Before the Threads are started, the IsBackground property is set to true. Each thread is then started and named (e.g., Thread1, Thread2, etc.). A message is displayed indicating that the thread is started, and then the main thread sleeps for 50 milliseconds before starting up the next thread.

After all three threads are started and another 50 milliseconds have passed, the first thread is aborted by calling Abort( ). The main thread then joins all three of the running threads. The effect of this is that the main thread will not resume until all the other threads have completed. When they do complete, the main thread prints a message: All my threads are done. The complete source is displayed in Example 20-2.

Example 20-2. Interrupting a thread
namespace Programming_CSharp
{
   using System;
   using System.Threading;

   class Tester
   {
      static void Main( )
      {
         // make an instance of this class
         Tester t = new Tester( );

         // run outside static Main
         t.DoTest( );                 
      }

      public void DoTest( )
      {
         // create an array of unnamed threads
         Thread[] myThreads = 
            {
               new Thread( new ThreadStart(Decrementer) ),
               new Thread( new ThreadStart(Incrementer) ),
               new Thread( new ThreadStart(Incrementer) )              
            };

         // start each thread
         int ctr = 1;
         foreach (Thread myThread in myThreads)
         {
            myThread.IsBackground=true;
            myThread.Start( );
            myThread.Name = "Thread" + ctr.ToString( );
            ctr++;
            Console.WriteLine("Started thread {0}", myThread.Name);
            Thread.Sleep(50);
         }

         // having started the threads
         // tell thread 1 to abort
         myThreads[1].Abort( );

         // wait for all threads to end before continuing
         foreach (Thread myThread in myThreads)
         {
            myThread.Join( );
         }

         // after all threads end, print a message
         Console.WriteLine("All my threads are done.");
      }

      // demo function, counts down from 1k
      public void Decrementer( )
      {
         try
         {
            for (int i = 1000;i>=0;i--)
            {
               Console.WriteLine(
                  "Thread {0}. Decrementer: {1}", 
                  Thread.CurrentThread.Name,
                  i);
               Thread.Sleep(1);
            }
         }
         catch (ThreadAbortException)
         {
            Console.WriteLine(
               "Thread {0} aborted! Cleaning up...",
               Thread.CurrentThread.Name);
         }
         finally
         {
            Console.WriteLine(
               "Thread {0} Exiting. ",
               Thread.CurrentThread.Name);
         }
      }

      // demo function, counts up to 1K
      public void Incrementer( )
      {
         try
         {
            for (int i =0;i<1000;i++)
            {
               Console.WriteLine(
                  "Thread {0}. Incrementer: {1}", 
                  Thread.CurrentThread.Name,
                  i);
               Thread.Sleep(1);
            }
         }
         catch (ThreadAbortException)
         {
            Console.WriteLine(
               "Thread {0} aborted! Cleaning up...",
               Thread.CurrentThread.Name);
         }
         finally
         {
            Console.WriteLine(
               "Thread {0} Exiting. ",
               Thread.CurrentThread.Name);
         }
      }
   }
}

Output (excerpt):
Started thread Thread1
Thread Thread1. Decrementer: 1000
Thread Thread1. Decrementer: 999
Thread Thread1. Decrementer: 998
Started thread Thread2
Thread Thread1. Decrementer: 997
Thread Thread2. Incrementer: 0
Thread Thread1. Decrementer: 996
Thread Thread2. Incrementer: 1
Thread Thread1. Decrementer: 995
Thread Thread2. Incrementer: 2
Thread Thread1. Decrementer: 994
Thread Thread2. Incrementer: 3
Started thread Thread3
Thread Thread1. Decrementer: 993
Thread Thread2. Incrementer: 4
Thread Thread2. Incrementer: 5
Thread Thread1. Decrementer: 992
Thread Thread2. Incrementer: 6
Thread Thread1. Decrementer: 991
Thread Thread3. Incrementer: 0
Thread Thread2. Incrementer: 7
Thread Thread1. Decrementer: 990
Thread Thread3. Incrementer: 1
Thread Thread2 aborted! Cleaning up...
Thread Thread2 Exiting.
Thread Thread1. Decrementer: 989
Thread Thread3. Incrementer: 2
Thread Thread1. Decrementer: 988
Thread Thread3. Incrementer: 3
Thread Thread1. Decrementer: 987
Thread Thread3. Incrementer: 4
Thread Thread1. Decrementer: 986
Thread Thread3. Incrementer: 5
// ...
Thread Thread1. Decrementer: 1
Thread Thread3. Incrementer: 997
Thread Thread1. Decrementer: 0
Thread Thread3. Incrementer: 998
Thread Thread1 Exiting.
Thread Thread3. Incrementer: 999
Thread Thread3 Exiting.
All my threads are done.

You see the first thread start and decrement from 1000 to 998. The second thread starts, and the two threads are interleaved for a while until the third thread starts. After a short while, however, Thread2 reports that it has been aborted, and then it reports that it is exiting. The two remaining threads continue until they are done. They then exit naturally, and the main thread, which was joined on all three, resumes to print its exit message.

    [ Team LiB ] Previous Section Next Section