5.7. System.Threading

Multithreading allows a process to execute multiple tasks in parallel. Each task is given its own thread of control. For example, in our text query system, we might choose to have each independent subquery executed simultaneously by spawning a thread for the evaluation of each. We could then either pause our controlling program (or main thread) until all the evaluation threads are complete, or perform auxiliary tasks while the executing threads run to completion.

One benefit of multithreading is the potential increase in performance. In a single-threaded program, the time cost of the query evaluation is the sum of the evaluation of each subquery, each executed one after the other in turn. In a multithreaded program, the time cost is now reduced to the time cost of the subquery that takes the longest time to evaluate, since all the queries are run in parallel, plus the overhead incurred by the support for multithreading.

A second benefit of multithreading is the ability of the main thread to package and deploy subtasks into parallel threads of execution. This frees the main thread to maintain communications with and monitoring of its users.

The primary drawback of multithreaded programming is the need to maintain the integrity of shared data objects and resources, such as forms, files, or even the system console. As we'll see, monitors help keep the multiple threads synchronized in their access to critical sections of our programs.

Thread support is provided within the System.Threading namespace. It includes classes such as Thread, Mutex, Monitor, Timer, and ThreadPool. First let's look at a simple program exercising the Thread interface. The program needs to create two threads—one responsible for writing the word ping, the other responsible for writing the word PONG:[1].

[1] This program is a variation of an excellent threads example in The Java™ Programming Language by Ken Arnold and James Gosling, Addison-Wesley, 1996.

					using System.Threading;
public static void Main()
{
    PingPong p1 = new PingPong( "ping", 33 );
    Thread ping = new Thread( new ThreadStart( p1.play ))
    ping.Start();

    while ( ! ping.IsAlive )
            Console.Write( "." );

    // OK: now Main() and ping are executing in parallel

    PingPong p2 = new PingPong( "PONG", 100 );
    Thread PONG = new Thread( new ThreadStart( p2.play ));
    PONG.Start();

    // OK: now Main(), ping, and PONG are executing ...

    // OK: let's rest this puppy for 100 milliseconds;
    //     both ping and PONG continue to run in parallel
    Thread.Sleep( 100 );

    /*
     * OK: another way of resting this puppy
     *
     * this main thread waits until either
     *      ping completes or 100 milliseconds pass ...
     *
     * the PONG thread is unaffected;
     * both threads continue to run in parallel
     */
    ping.Join(100);


    // let's suspend PONG for a moment while we determine
    // if ping completed or Join() timed out ...
    PONG.Suspend();

    if ( p1.Count != PingPong.Max )
         // Join() timed out ...
         // ping must still be executing

    // OK: let's resume PONG
    PONG.Resume();
    // let's absolutely wait for PONG to complete
    PONG.Join();

    Console.WriteLine( "OK: ping count: {0}", p1.Count );
    Console.WriteLine( "OK: PONG count: {0}", p2.Count );
}

Each PingPong class object is initialized with its display string and a delay time, in milliseconds, between each display:

PingPong p1 = new PingPong( "ping", 33 );
PingPong p2 = new PingPong( "PONG", 100 );

play() is a PingPong class method that prints out the associated string. The only unfamiliar aspect of its implementation is its use of the static Sleep() method of the Thread class to accomplish the millisecond delay:

public void play()
{
    for ( ; ; )
    {
        Console.Write( theWord+ " " );

        Thread.Sleep( theDelay );
        if ( ++theCount == theMax )
             return;
    }
}

theCount keeps a running count of the displayed instances. theMax is a const member set to the maximum number of instances to display.

Sleep() suspends the current thread for a specified time, in milliseconds. The current thread is, broadly put, whatever is currently executing. When play() is invoked by the ping thread, ping, of course, is the current thread, theDelay is set to 33, and ping sleeps for the duration.

The current thread is not always a named thread object such as we're discussing here. For example, if we don't use threads at all in our program, we don't call it a no-threaded program. We call it a single-threaded program. The main thread of control begins with the invocation of Main(). In our program, when we invoke Sleep() within Main(), as shown here:

public static void Main()
{
    PingPong p1 = new PingPong( "ping", 33 );
    Thread ping = new Thread( new ThreadStart( p1.play ))
    ping.Start();

    Thread.Sleep( 100 );
}

it's the main thread, not ping, that goes to sleep for 100 milliseconds.

The Thread constructor takes a single argument—a delegate type named ThreadStart. When a thread begins, it invokes the method addressed by ThreadStart. Both our thread objects are initialized to invoke play(), through either the p1 or the p2 PingPong class object:

Thread ping = new Thread( new ThreadStart( p1.play ));
Thread PONG = new Thread( new ThreadStart( p2.play ));

A thread does not begin execution until we explicitly invoke Start():

ping.Start();

When ping starts execution, it invokes play() through p1. When play() completes execution, ping ceases to exist. We describe it as dead.

What can we do with a thread once we start it? There are several options:

  • We can ignore it completely—just let it run its course. We don't have to do anything with it once we have kicked it off by invoking Start().

  • We can stop it by invoking Suspend(). Suspended threads are collected somewhere where they won't bother us.

  • If it has been suspended, we can start it back up by invoking Resume(). If it hasn't been suspended, invoking Resume() causes no harm. We can invoke Suspend() on a suspended thread without penalty. Similarly, we can invoke Resume() without penalty on a thread that is not suspended.

  • If suspension is too severe, we can put the thread to sleep by invoking Sleep(), but only if it is the current thread. There is no instance Sleep() method that can be applied to a named thread.

  • We can query a thread as to whether it is alive or not through its IsAlive property. IsAlive returns true only if the thread has been started and is not yet dead. A thread dies after it has completed executing its ThreadStart object. We can directly kill a thread by invoking Abort().

Through the Join() method we can also have the current thread wait until a particular thread completes execution and terminates:

public static void Main()
{

      // OK: we resume PONG
      PONG.Resume();

      // OK: we sit here until PONG completes
      PONG.Join();
}

In this example, Main() is told absolutely to wait and not move until PONG has completed and presumably made available the results of its execution. Now if PONG were infinitely recurring, of course, Main() might never stop waiting. So sometimes we may want to add a condition to our imperative by providing Join() with an explicit time limit to wait:

// OK: let's rest this puppy for 100 milliseconds;
//     both ping and PONG continue to run in parallel
Thread.Sleep( 100 );

/*
 * OK: another way of resting this puppy
 *
 * this main thread waits until either
 *      ping completes or 100 milliseconds pass ...
 *
 * the PONG thread is unaffected;
 * both threads continue to run in parallel
 */

ping.Join(100);

The hard part of thread programming is maintaining the integrity of memory and other shared resources. For example, here is a portion of the program output. The nonbold text beginning with OK is generated from Main(). The text highlighted in bold is generated by the ping and PONG asynchronous threads:

OK: about to start ping thread
OK: ping thread is now alive!
OK: ping is now running in parallel
OK: 0 Within PingPong.play() for ping!!!
					ping 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

OK: creating PONG object
OK: creating PONG thread object
OK: about to start PONG thread
OK: ping and PONG are now running in parallel
OK: 0 1 2 3 4 5 6 Within PingPong.play() for PONG!!!
					PONG 7 8 9 10 11 12 13 14 15 16 17 18 19

OK: about to put main thread to sleep for 600 milliseconds
ping ping PONG ping ping ping PONG ping ping PONG
					ping ping ping PONG ping ping PONG ping ping ping
					PONG OK: here after sleep

OK: about to wait to Join(400) ping
ping ping PONG ping ping ping PONG ping ping PONG
					ping ping ping OK: back now -- hi.

In this program, the interleaving of output among the three parallel threads is amusing—well, somewhat amusing, anyway. In real-world code, of course, we need to guarantee the integrity of resources from the competing demands of parallel independent threads. One way to do that is with monitors.

The Monitor class allows us to synchronize thread access to critical code blocks through an Enter() and Exit() lock and unlock pair of methods—for example,

Monitor.Enter( this );
try
{
     // critical text goes here ...
}
finally{
   Monitor.Exit( this );
}

When the static Enter() method is invoked, it asks for a lock to associate with the object passed to it. If the object is already associated with a lock, the thread is not permitted to continue execution. We say that it blocks.

If the object currently lacks a lock, however, the lock is acquired, and the code following Enter() is executed. Until the lock is no longer associated with the object, our thread has exclusive access to the code following Enter().

The static Exit() method removes the lock on the object passed to it. If one or more threads are waiting for the lock to be removed, one of them is unblocked and allowed to proceed. If we fail to provide an Enter()/Exit() pair using the same object, the code segment could remain locked indefinitely.

TryEnter() does not block, or blocks only for a specified time in milliseconds, before giving up and returning false—for example,

if ( ! Monitor.TryEnter( fout, maxWait ))
   { logFailure( file_name ); return;  }

Monitor allows us to guarantee exclusive access to a critical block of code by associating a lock with an object. C# provides an alternative shorthand notation for the Monitor.Enter()/Monitor.Exit() pair of calls through a use of the lock keyword. For example, the following code segment:

// equivalent to the earlier
// Monitor.Enter()/Monitor.Exit() code block
lock( this );
{
     // critical text goes here ...
}

is equivalent to the earlier code block that begins with Monitor.Enter().

Let me end with a brief mention of the ThreadPool class. It manages a pool of preallocated threads. ThreadPool allows us to associate a callback method with the wait state of an object. When the wait is open, one of the available pool threads invokes the associated callback method. Alternatively, we can simply queue a method to be added to the thread pool and invoked when a thread becomes available.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.36.38