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-ofmilliseconds
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.
7
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
or WPF 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.
8
Creating
and Starting Threads
Threads
are created using the Thread class’s constructor, passing in a ThreadStart
delegate –
indicating
the method where execution should begin. Here’s how the ThreadStart delegate
is
defined:
public delegate void ThreadStart();
Calling
Start on the thread then sets it running. The thread continues until its
method returns, at
which
point the thread ends. Here’s an example, using the expanded C# syntax for
creating a
TheadStart
delegate:
class ThreadTest {
static void Main()
{
Thread t
= new Thread (new ThreadStart (Go));
t.Start(); // Run
Go() on the new thread.
Go(); //
Simultaneously run Go() in the main thread.
}
static void Go()
{ Console.WriteLine ("hello!");
}
In
this example, thread t executes Go() – at (much) the same time
the main thread calls Go(). The
result
is two near-instant hellos:
hello!
hello!
A
thread can be created more conveniently using C#'s shortcut syntax for
instantiating delegates:
static void Main()
{
Thread t
= new Thread (Go);
// No need to explicitly use ThreadStart
t.Start();
...
}
static void Go()
{ ... }
In
this case, a ThreadStart delegate is inferred automatically by the
compiler. Another shortcut
is
to use an anonymous method to start the thread:
static void Main()
{
Thread t
= new Thread (delegate() { Console.WriteLine
("Hello!"); });
t.Start();
}
A
thread has an IsAlive property that returns true after its Start() method
has been called, up
until
the thread ends.
A
thread, once ended, cannot be re-started.
No comments:
Post a Comment
Note: only a member of this blog may post a comment.