Chapter 14 discussed delegates and events. Programming multiple threads with C# depends heavily on the syntax of delegates. In order to start a new thread, it is necessary to call a delegate that contains the code for the separate thread. Listing 15.1 provides a simple example, and Output 15.1 on page 554 shows the results.
Listing 15.1. Starting a Method in a Separate Thread
The code that is to run in a new thread appears in the DoWork() method. This method prints. to the console repeatedly during each iteration within a loop. Besides the fact that it contains code for starting another thread, the Main() method is virtually identical in structure to DoWork(), except that it displays -. The resulting output from the program is a series of dashes until the thread context switches, at which time the program displays periods until the next thread switch, and so on. (On Windows, it is possible to increase the chances of a thread context switch by using Start /low /b <program.exe> to execute the program. This will assign the entire process a lower priority, causing its threads to be interrupted more frequently, and thus causing more frequent thread switches.)
Starting a Thread
In order for DoWork() to run under the context of a different thread, you must first instantiate a System.Threading.ThreadStart delegate around the DoWork() method. Next, you pass the THReadStart delegate instance to the System.Threading.Thread constructor before commencing execution of the thread with a call to tHRead.Start().
In Listing 15.1, you instantiate the thread in two separate steps, explicitly instantiating a System.Threading.ThreadStart instance and assigning it to a variable before instantiating the System.Threading.Threadobject. As Listing 15.2 demonstrates, you could combine both statements, or you could use C# 2.0's delegate inference to avoid any explicit syntax to instantiate ThreadStart and instead pass DoWork directly into System .Threading.Thread's constructor.
Listing 15.2. Creating a Thread Using C# 2.0 Syntax
You can elect not to declare a ThreadStart variable in C# 1.0, but you cannot avoid explicit instantiation of the THReadStart instance.
Starting a thread simply involves a call to Thread.Start(). As soon as the DoWork() method begins execution, the call to THRead.Start() returns and executes the for loop in the Main() method. The threads are now independent and neither waits for the other. The output from Listing 15.1 and Listing 15.2 will intermingle the output of each thread, instead of creating a series of . followed by -.
Threads include a number of methods and properties for managing their execution.
Once threads are started, you can cause a "wait for completion" with a call to thread.Join(). The calling thread will wait until the tHRead instance terminates. The Join() method is overloaded to take either an int or a TimeSpan to support a maximum time to wait for thread completion before continuing execution.
Another thread configuration option is the thread.IsBackGround property. By default, a thread is a foreground thread, meaning the process will not terminate until the thread completes. In contrast, setting the IsBackGround property to true will allow process execution to terminate prior to a thread's completion.
When using the Join() method, you can increase or decrease the thread's priority by setting the Priority to a new ThreadPriority enum value (Lowest, BelowNormal, Normal, AboveNormal, Highest).
A thread's state is accessible through the ThreadState property, a more precise reflection of the Boolean IsAlive property. The ThreadState enum flag values are Aborted, AbortRequested, Background, Running, Stopped, StopRequested, Suspended, SuspendRequested, Unstarted, and WaitSleepJoin. The flag names indicate activities that may occur on a thread. Two noteworthy methods are THRead.Sleep() and Abort().
THRead.Sleep() is a static method that pauses the current thread for a period. A single parameter (in milliseconds, or a TimeSpan) specifies how long the active thread waits before continuing execution. This enables switching to a different thread for a specific period.
This method is not for accurate timing. Returns can occur hundreds of milliseconds before or after the specified time.
A thread's Abort() method causes a ThreadAbortException to be thrown within the target thread. The problem is that THRead.Abort() introduces uncertainty into the thread's behavior. In .NET 1.x, if the abort interrupts execution of a finally block, the remaining code within that block will never run. Furthermore, Abort() may fail because the aborted thread could have a catch block that handles the ThreadAbortException and calls Thread.ResetAbort(), or the thread could currently be running unmanaged code which will not throw the THReadAbortException until the code returns. Except in rare circumstances, developers should consider the Abort() method to be a last resort.
In .NET 2.0, if the abort interrupts execution of a finally block, then its effect will be delayed until the conclusion of the finally block (and any additional finally blocks within the call stack).