(Post 09/10/2007) The following tables summarize
the .NET tools available for coordinating or synchronizing the actions
of threads:
Simple Blocking Methods
Construct |
Purpose |
Sleep |
Blocks for a given time period |
Join |
Waits for another thread to finish |
Locking Constructs
Construct |
Purpose |
Cross-Process? |
Speed |
lock |
Ensures just one thread can access a resource, or section of code |
no |
fast |
Mutex |
Ensures just one thread can access a resource, or section of code.Can
be used to prevent multiple instances of an
application from starting |
yes |
moderate |
Semaphore |
Ensures not more than a specified number of threads can access
a resource, or section of code. |
yes |
moderate |
(Synchronization Contexts
are also provided, for automatic locking).
Signaling Constructs
Construct |
Purpose |
Cross-Process? |
Speed |
EventWaitHandle |
Allows a thread to wait until it receives a signal from another
thread |
yes |
moderate |
Wait and Pulse* |
Allows a thread to wait until a custom blocking condition is met |
no |
moderate |
Non-Blocking Synchronization Constructs*
Construct |
Purpose |
Cross-Process? |
Speed |
Interlocked* |
To perform simple non-blocking atomic operations |
yes (assuming shared memory) |
very fast |
volatile* |
To allow safe non-blocking access to individual fields outside
of a lock |
very fast |
*Covered in Part 4
Blocking
When a thread waits or pauses as a result of using the
constructs listed in the tables above, it's said to be blocked. Once blocked,
a thread immediately relinquishes its allocation of CPU time, adds WaitSleepJoin
to its ThreadState property, and doesn’t
get re-scheduled until unblocked. Unblocking happens in one of four ways
(the computer's power button doesn't count!):
- by the blocking condition being satisfied
- by the operation timing out (if a timeout is specified)
- by being interrupted via Thread.Interrupt
- by being aborted via Thread.Abort
A thread is not deemed blocked if its execution is paused
via the (deprecated) Suspend method.
Sleeping and Spinning
Calling Thread.Sleep blocks
the current thread for the given time period (or until interrupted):
static void Main() {
Thread.Sleep (0); // relinquish CPU time-slice
Thread.Sleep (1000); // sleep for 1000 milliseconds
Thread.Sleep (TimeSpan.FromHours (1)); // sleep for 1 hour
Thread.Sleep (Timeout.Infinite); // sleep until interrupted
}
More precisely, Thread.Sleep relinquishes
the CPU, requesting that the thread is not re-scheduled until the given
time period has elapsed. Thread.Sleep(0) relinquishes the CPU just long
enough to allow any other active threads present in a time-slicing queue
(should there be one) to be executed.
Thread.Sleep is unique amongst
the blocking methods in that suspends Windows message pumping
within a Windows Forms application, or COM environment on a thread
for which the single-threaded apartment
model is used. This is of little consequence with
Windows Forms applications, in that any lengthy blocking operation
on the main UI thread will make the application unresponsive –
and is hence generally avoided – regardless of the whether or
not message pumping is "technically" suspended. The
situation is more complex in a legacy COM hosting environment,
where it can sometimes be desirable to sleep while keeping message
pumping alive. Microsoft's Chris Brumme discusses this at length
in his web log (search:
'COM "Chris Brumme"'). |
The Thread class also provides a SpinWait
method, which doesn’t relinquish any CPU time, instead looping the CPU
– keeping it “uselessly busy” for the given number of iterations. 50 iterations
might equate to a pause of around a microsecond, although this depends
on CPU speed and load. Technically, SpinWait is not a
blocking method: a spin-waiting thread does not have a ThreadState
of WaitSleepJoin and can’t be prematurely Interrupted
by another thread. SpinWait is rarely used – its primary
purpose being to wait on a resource that’s expected to be ready very soon
(inside maybe a microsecond) without calling Sleep and
wasting CPU time by forcing a thread change. However this technique is
advantageous only on multi-processor computers: on single-processor computers,
there’s no opportunity for a resource’s status to change until the spinning
thread ends its time-slice – which defeats the purpose of spinning to
begin with. And calling SpinWait often or for long periods
of time itself is wasteful on CPU time.
Blocking vs. Spinning
A thread can wait for a certain condition by explicitly
spinning using a polling loop, for example:
while (!proceed);
or:
while (DateTime.Now < nextStartTime);
This is very wasteful on CPU time: as far as the CLR
and operating system is concerned, the thread is performing an important
calculation, and so gets allocated resources accordingly! A thread looping
in this state is not counted as blocked, unlike a thread waiting on an
EventWaitHandle (the construct
usually employed for such signaling tasks).
A variation that's sometimes used is a hybrid between
blocking and spinning:
while (!proceed) Thread.Sleep
(x); // "Spin-Sleeping!"
The larger x, the more CPU-efficient this is; the trade-off
being in increased latency. Anything above 20ms incurs a negligible overhead
– unless the condition in the while-loop is particularly complex.
Except for the slight latency, this combination of spinning
and sleeping can work quite well (subject to concurrency issues on the
proceed flag, discussed
in Part 4). Perhaps its biggest use is when a programmer
has given up on getting a more complex signaling
construct to work!
Joining a Thread
You can block until another thread ends by calling Join:
class JoinDemo {
static void Main() {
Thread t = new Thread (delegate()
{ Console.ReadLine(); });
t.Start();
t.Join(); // Wait until thread t finishes
Console.WriteLine ("Thread t's ReadLine complete!");
}
}
The Join method also accepts a timeout
argument – in milliseconds, or as a TimeSpan, returning
false if the Join timed out rather than found the end
of the thread. Join with a timeout functions rather like
Sleep – in fact the following two lines of code are almost
identical:
Thread.Sleep (1000);
Thread.CurrentThread.Join (1000);
(Their difference is apparent only in single-threaded
apartment applications with COM interoperability, and stems from the subtleties
in Windows message pumping semantics described previously: Join keeps
message pumping alive while blocked; Sleep suspends message pumping).
(Sưu tầm) |