2. Blocking Threads
The Thread
class provides a number of methods you can use to block the execution
of a thread, similar in their effect to the native mechanisms available
to Windows programmers. These include suspending a thread, putting a
thread to sleep, waiting for a thread to die. Developers often misuse
these mechanisms without ever realizing they were doing anything wrong.
This section outlines the various blocking options and discusses why it's a bad idea to use most of them.
2.1. Suspending and resuming a thread
The Thread class provides the Suspend( ) method, which suspends the execution of a thread, and the Resume( ) method, which resumes a suspended thread:
public sealed class Thread
{
public void Resume( );
public void Suspend( );
//Other methods and properties
}
Anybody can call Suspend( ) on a Thread object, including objects running on that thread, and there is no harm in calling Suspend( ) on an already suspended thread. Obviously, only clients on other threads can resume a suspended thread. Suspend( )
is a non-blocking call, meaning that control returns immediately to the
caller and the thread is suspended later, usually at the next safe
point. A safe point is a point in the code where it's safe for garbage collection to take place. The JIT compiler identifies those points in
the code that are safe for suspending the thread (such as when
returning from method calls or branching for another loop iteration).
When Suspend( ) is called, the thread is suspended once it reaches the next safe point.
The
bottom line is that suspending a thread isn't an instantaneous
operation. The need to suspend and then resume a thread usually results
from a need to synchronize the execution of that thread with other
threads, but using Suspend( ) and Resume( ) for
that purpose isn't recommended because there is no telling when these
operations will take place. Consequently, .NET 2.0 applies the Obsolete attribute to Suspend( ) and Resume( ),
warning you not to use them. If you need to suspend the execution of a
thread and then resume it later, you should use the dedicated .NET
synchronization objects (described later). The synchronization objects
provide a deterministic way of blocking a thread or signaling it to
continue executing. In general, you should avoid explicitly suspending
and resuming threads.
2.2. Putting a thread to sleep
The Thread class provides two overloaded versions of the static Sleep( ) method, which puts a thread to sleep for a specified timeout:
public sealed class Thread
{
public static void Sleep(int millisecondsTimeout);
public static void Sleep(TimeSpan timeout);
//Other methods and properties
}
Because Sleep( ) is a static method, you can put only your own thread to sleep:
Thread.Sleep(20);//Sleep for 20 milliseconds
Sleep( ) is a blocking call, meaning that control returns to the calling thread only after the sleep period has elapsed. Sleep( ) puts the thread in a special queue of threads waiting to be awakened by the operating system. Any thread that calls Sleep( )
willingly relinquishes the remainder of its allocated CPU time slot,
even if the sleep timeout is less than the remainder of the time slot.
Consequently, calling Sleep( ) with a timeout of zero is a way to force a thread context switch:
Thread.Sleep(0);//Forces a context switch
If
no other thread with the same or higher priority is ready to run,
control returns to the thread .
You can also put a thread to sleep indefinitely, using the Infinite static constant of the Timeout class:
Thread.Sleep(Timeout.Infinite);
Of
course, putting a thread to sleep indefinitely is an inefficient use of
the system services; it's better to simply terminate the thread (by
returning from the thread method). If you need to block a thread until
some event takes place, use .NET synchronization objects. In fact, you
should generally avoid putting a thread to sleep, unless you
specifically want the thread to act as a kind of timer. Traditionally,
you put threads to sleep to cope with race conditions, by explicitly removing some of the threads involved in the race condition. A race condition
is a situation in which thread T1 needs to have another thread, T2,
complete a task or reach a certain state. The race condition occurs
when T1 proceeds as if T2 is ready, when in fact it may not be.
Sometimes T1 has its own processing to do, and that (in a poorly
designed system) usually keeps it busy long enough to avoid the race
condition. Occasionally, however, T1 will complete before T2 is ready,
and an error will occur. Using Sleep( ) to resolve a race
condition is inappropriate, because it doesn't address the root cause
of the race condition (usually, the lack of proper synchronization
between the participating threads). Putting threads to sleep is at best
a makeshift solution, because the race condition can still manifest
itself in different ways; also, it isn't likely to work when more
threads get involved. Avoid putting a thread to sleep, and use .NET
synchronization objects instead.
Traditionally,
most APIs in Windows that deal with time use some form of physical time
measurement, such as seconds or milliseconds. You probably have no
problem converting a minute or two to seconds. However, it's harder to
convert 1 hour and 48 minutes into seconds, or 2 days. The TimeSpan
struct addresses this issue by providing many methods for time
conversion and representing time periods in a uniform manner. For
example, if you need to represent 2 days, use the static method FromDays( ), which returns a TimeSpan value representing 2 days:
TimeSpan TimeSpan = TimeSpan.FromDays(2);
|
2.3. Spinning while waiting
The Thread class provides another sleep-like operation, called SpinWait( ):
public static void SpinWait(int iterations);
When a thread calls SpinWait( ),
the calling thread waits the number of iterations specified but is
never added to the queue of waiting threads. As a result, the thread is
effectively put to sleep without relinquishing the remainder of its CPU
time slot. The .NET documentation doesn't define what an iteration is,
but it's likely mapped to a predetermined number (probably just one) of
no-operation (NOP) assembly instructions. Consequently, the following SpinWait( ) instruction will take a different amount of time to complete on machines with different CPU clock speeds:
int long Million = 1000000;
Thread.SpinWait(Million);
SpinWait( ) isn't intended to replace Sleep( ),
but rather is available as an advanced optimization technique. If you
know that some resource your thread is waiting for will become
available in the immediate future, it's potentially more efficient to
spin and wait than it would be to use either Sleep( ) or a
synchronization object, because these force a thread context switch,
which is one of the most expensive operations performed by the
operating system. However, even in the esoteric cases for which SpinWait( ) was designed, using it amounts to an educated guess at best. SpinWait( )
gains you nothing if the resource isn't available at the end of the
call, or if the operating system preempts your thread because its time
slot has elapsed or because another thread with a higher priority is
ready to run. In general, I recommend that you always use deterministic
programming (synchronization objects, in this case) and avoid
optimization techniques.
2.4. Joining a thread
The Thread class's Join( ) method allows one thread to wait for another thread to terminate. Any client that has a reference to a Thread object can call Join( ) and have the client thread blocked until the thread terminates:
static void WaitForThreadToDie(Thread thread)
{
thread.Join( );
}
Join( )
returns regardless of the cause of death—either natural (the thread
returns from the thread method) or unnatural (the thread encounters an
exception).
Note that it is imperative to always check before calling Join( ) that you are not joining your own thread:
static void WaitForThreadToDie(Thread thread)
{
Debug.Assert(Thread.CurrentThread.ManagedThreadId !=
thread.ManagedThreadId);
thread.Join( );
}
Doing so will prevent a deadlock of waiting for your own thread to die. Join( )
is useful when dealing with application shutdown; when an application
starts its shutdown procedure, it typically signals all the worker
threads to terminate and then waits for the threads to terminate before
proceeding with the shutdown. The standard way of doing this is to call
Join( ) on the worker threads. Calling Join( ) is similar to waiting on a thread handle in the Win32 world, and it's likely that the Join( ) method implementation does just that.
The Join( ) method has two overloaded versions, allowing you to specify a waiting timeout:
public sealed class Thread
{
public void Join( );
public bool Join(int millisecondsTimeout);
public bool Join(TimeSpan timeout);
//Other methods and properties
}
When you specify a timeout, Join( ) returns when the timeout has expired or when the thread is terminated, whichever happens first. The bool return value is set to false if the timeout has elapsed but the thread is still running, and to true if the thread is dead.
2.5. Interrupting a waiting thread
You can rudely awaken a sleeping or waiting thread by calling the Interrupt( ) method of the Thread class:
public void Interrupt( );
Calling Interrupt( ) unblocks a sleeping thread (or a waiting thread, such as a thread that called Join( ) on another thread) and throws an exception of type ThreadInterruptedException in the unblocked thread. If the code the thread executes doesn't catch that exception, the thread is terminated by the runtime.
If a call to Thread.Interrupt( )
is made on a thread that isn't sleeping or waiting, the next time the
thread tries to go to sleep or wait .NET immediately throws an
exception of type ThreadInterruptedException in its call stack. Note, however, that calling Interrupt( )
doesn't interrupt a thread that is executing unmanaged code via
interop; nor does it interrupt a thread that is in the middle of a call
to SpinWait( ), because as far as the operating system is concerned that thread is not actually waiting at all.
Again,
you should avoid relying on drastic solutions such as throwing
exceptions to unblock another thread. Use .NET synchronization objects
instead, to gain the benefits of structured and deterministic code flow.