(Post 02/10/2007) C# supports parallel execution
of code through multithreading. A thread is an independent execution path,
able to run simultaneously with other threads.
A C# program starts in a single thread created automatically
by the CLR and operating system (the "main" thread), and is
made multi-threaded by creating additional threads. Here's a simple example
and its output:
All examples assume the following namespaces are imported,
unless otherwise specified:
using System;
using System.Threading;
class ThreadTest {
static void Main() {
Thread t = new Thread (WriteY);
t.Start(); // Run WriteY on the new thread
while (true) Console.Write ("x"); // Write 'x' forever
}
static void WriteY() {
while (true) Console.Write ("y"); // Write 'y' forever
}
}
The main thread creates a new thread t
on which it runs a method that repeatedly prints the character y.
Simultaneously, the main thread repeatedly prints the character x.
The CLR assigns each thread its own memory stack so that
local variables are kept separate. In the next example, we define a method
with a local variable, then call the method simultaneously on the main
thread and a newly created thread:
static void Main() {
new Thread (Go).Start(); // Call Go() on a new thread
Go(); // Call Go() on the main thread
}
static void Go() {
// Declare and use a local variable - 'cycles'
for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
}
A separate copy of the cycles
variable is created on each thread's memory stack, and so the output is,
predictably, ten question marks.
Threads share data if they have a common reference to
the same object instance. Here's an example:
class ThreadTest {
bool done;
static void Main() {
ThreadTest tt = new ThreadTest(); // Create a common instance
new Thread (tt.Go).Start();
tt.Go();
}
// Note that Go is now an instance method
void Go() {
if (!done) { done = true; Console.WriteLine ("Done"); }
}
}
Because both threads call Go()
on the same ThreadTest instance,
they share the done field.
This results in "Done" being printed once instead of twice:
Static fields offer another way to share data between
threads. Here's the same example with done as a static field:
class ThreadTest {
static bool done; // Static fields are shared between all threads
static void Main() {
new Thread (Go).Start();
Go();
}
static void Go() {
if (!done) { done = true; Console.WriteLine ("Done"); }
}
}
Both of these examples illustrate another key concept
– that of thread safety
(or, rather, lack of it!) The output is actually indeterminate: it's possible
(although unlikely) that "Done" could be printed twice. If,
however, we swap the order of statements in the Go
method, then the odds of "Done" being printed twice go up dramatically:
static void Go() {
if (!done) { Console.WriteLine ("Done"); done = true; }
}
The problem is that one thread can be evaluating the
if statement right as the
other thread is executing the WriteLine
statement – before it's had a chance to set done
to true.
The remedy is to obtain an exclusive
lock while reading and writing to the common field. C#
provides the lock statement
for just this purpose:
class ThreadSafe {
static bool done;
static object locker = new object();
static void Main() {
new Thread (Go).Start();
Go();
}
static void Go() {
lock (locker) {
if (!done) { Console.WriteLine ("Done"); done = true; }
}
}
}
When two threads simultaneously contend a lock (in this
case, locker), one thread
waits, or blocks, until
the lock becomes available. In this case, it ensures only one thread can
enter the critical section of code at a time, and "Done" will
be printed just once. Code that's protected in such a manner – from indeterminacy
in a multithreading context – is called thread-safe.
Temporarily pausing, or blocking, is an essential feature
in coordinating, or synchronizing the activities of threads. Waiting for
an exclusive lock is one reason for which a thread can block. Another
is if a thread wants to pause, or Sleep
for a period of time:
Thread.Sleep (TimeSpan.FromSeconds
(30)); // Block for 30 seconds
A thread can also wait for another thread to end, by
calling its Join method:
Thread t = new Thread (Go);
// Assume Go is some static method
t.Start();
t.Join(); // Wait (block) until thread t ends
A thread, while blocked, doesn't consume CPU resources.
How Threading Works
Multithreading is managed internally by a thread scheduler,
a function the CLR typically delegates to the operating system. A thread
scheduler ensures all active threads are allocated appropriate execution
time, and that threads that are waiting or blocked – for instance – on
an exclusive lock, or on user input – do not consume CPU time.
On a single-processor computer, a thread scheduler performs
time-slicing – rapidly switching execution between each of the active
threads. This results in "choppy" behavior, such as in the very
first example, where each block of a repeating X or Y character corresponds
to a time-slice allocated to the thread. Under Windows XP, a time-slice
is typically in the tens-of-milliseconds region – chosen such as to be
much larger than the CPU overhead in actually switching context between
one thread and another (which is typically in the few-microseconds region).
On a multi-processor computer, multithreading is implemented
with a mixture of time-slicing and genuine concurrency – where different
threads run code simultaneously on different CPUs. It's almost certain
there will still be some time-slicing, because of the operating system's
need to service its own threads – as well as those of other applications.
A thread is said to be preempted when its execution is
interrupted due to an external factor such as time-slicing. In most situations,
a thread has no control over when and where it's preempted.
Threads vs. Processes
All threads within a single application are logically
contained within a process – the operating system unit in which an application
runs.
Threads have certain similarities to processes – for
instance, processes are typically time-sliced with other processes running
on the computer in much the same way as threads within a single C# application.
The key difference is that processes are fully isolated from each other;
threads share (heap) memory with other threads running in the same application.
This is what makes threads useful: one thread can be fetching data in
the background, while another thread is displaying the data as it arrives.
When to Use Threads
A common application for multithreading is performing
time-consuming tasks in the background. The main thread keeps running,
while the worker thread does its background job. With Windows Forms applications,
if the main thread is tied up performing a lengthy operation, keyboard
and mouse messages cannot be processed, and the application becomes unresponsive.
For this reason, it’s worth running time-consuming tasks on worker threads
even if the main thread has the user stuck on a “Processing… please wait”
modal dialog in cases where the program can’t proceed until a particular
task is complete. This ensures the application doesn’t get tagged as “Not
Responding” by the operating system, enticing the user to forcibly end
the process in frustration! The modal dialog approach also allows for
implementing a "Cancel" button, since the modal form will continue
to receive events while the actual task is performed on the worker thread.
The BackgroundWorker class
assists in just this pattern of use.
In the case of non-UI applications, such as a Windows
Service, multithreading makes particular sense when a task is potentially
time-consuming because it’s awaiting a response from another computer
(such as an application server, database server, or client). Having a
worker thread perform the task means the instigating thread is immediately
free to do other things.
Another use for multithreading is in methods that perform
intensive calculations. Such methods can execute faster on a multi-processor
computer if the workload is divided amongst multiple threads. (One can
test for the number of processors via the Environment.ProcessorCount
property).
A C# application can become multi-threaded in two ways:
either by explicitly creating and running additional threads, or using
a feature of the .NET framework that implicitly creates threads – such
as BackgroundWorker, thread
pooling, a threading timer,
a Remoting server, or a Web Services or ASP.NET application. In these
latter cases, one has no choice but to embrace multithreading. A single-threaded
ASP.NET web server would not be cool – even if such a thing were possible!
Fortunately, with stateless application servers, multithreading is usually
fairly simple; one's only concern perhaps being in providing appropriate
locking mechanisms around data cached in static variables.
When Not to Use Threads
Multithreading also comes with disadvantages. The biggest
is that it can lead to vastly more complex programs. Having multiple threads
does not in itself create complexity; it's the interaction between the
threads that creates complexity. This applies whether or not the interaction
is intentional, and can result long development cycles, as well as an
ongoing susceptibility to intermittent and non-reproducable bugs. For
this reason, it pays to keep such interaction in a multi-threaded design
simple – or not use multithreading at all – unless you have a peculiar
penchant for re-writing and debugging!
Multithreading also comes with a resource and CPU cost
in allocating and switching threads if used excessively. In particular,
when heavy disk I/O is involved, it can be faster to have just one or
two workers thread performing tasks in sequence, rather than having a
multitude of threads each executing a task at the same time. Later we
describe how to implement a Producer/Consumer
queue, which provides just this functionality.
(Sưu tầm) |