1. Threads and Multithreading
In modern computing terminology, a thread
is simply a path of execution within a process. Every application runs
on at least one thread, which is initialized when the process within
which the application runs is started up. The threads of an application
always execute within the context provided by the application process.
Typically, you find two kinds of operations in any application:
CPU-bound and I/O-bound operations. CPU-bound operations use the machine's central processing unit (CPU) to perform intensive or repetitious computations. I/O-bound
operations are tied to an input or output device such as a
user-interface peripheral (keyboard, screen, mouse, or printer), a hard
drive (or any non-memory durable storage), a network or communication
port, or any other hardware device.
It's
often useful to create multiple threads within an application, so that
operations that are different in nature can be performed in parallel
and the machine's CPU (or CPUs) and devices can be used as efficiently
as possible. An I/O-bound operation (such as disk access), for example,
can take place concurrently with a CPU-bound operation (such as the
processing of an image). As long as two I/O-bound operations don't use
the same I/O device (such as disk access and network-socket access),
having them run on two different threads will improve your
application's ability to efficiently handle these I/O devices and
increase the application's throughput and performance. In the case of
CPU-bound operations, on a single-CPU machine there is no performance
advantage to allocating two distinct threads to run separate CPU-bound
operations. However, you should definitely consider doing so on a
multi-CPU machine, because multithreading is the only way to use the
extra processing power available through each additional CPU.
Almost
all modern applications are multithreaded, and many of the features
users take for granted would not be possible otherwise. For example, a
responsive user interface implies multithreading. The application
processes user requests (such as printing or connecting to a remote
machine) on a different thread than the one employed for the user
interface. If the same thread were being used, the user interface would
appear to hang while the other requests were being processed. With
multithreading, because the user interface is on a different thread, it
remains responsive while the user's requests are being processed.
Multiple threads are also useful in applications that require high
throughput. When your application needs to process incoming client
requests as fast as it can, it's often advantageous to spin off a
number of worker threads to handle requests in parallel.
Do
not create multiple threads just for the sake of having them. You must
examine your particular case carefully and evaluate all possible
solutions. The decision to use multiple
threads can open a Pandora's box of thread-synchronization and
component-concurrency issues,. |
|
2. Components and Threads
A
component-oriented application doesn't necessarily need to be
multithreaded. The way in which an application is divided into
components is unrelated to how many execution threads it uses. In any
given application, you're as likely to have several components
interacting with one another on a single thread of execution as you are
to have multiple threads accessing a single component. What is special
about component-oriented applications has to do with the intricate
synchronization issues inherent in the nature of component development
and deployment.
3. Working with Threads
In
.NET, a thread is the basic unit of execution. .NET threads are managed
code representations of the underlying threads of the operating system.
Under the current version of .NET on Windows, .NET threads map
one-to-one to Win32 native threads. However, this mapping can be
changed in other hosts. For example, SQL Server 2005 is a managed CLR
host and is capable of using fibers for managed threads. The underlying host is considered to be Windows unless
otherwise specified. For each thread, the operating system allocates
registers, a program counter, a stack, and a stack pointer, and assigns
it a time slot and a priority. The operating system (presently) is the
one responsible for thread scheduling and thread context switches, as
well as thread-manipulation requests such as start and sleep. .NET
exposes some of the native thread properties (such as priority). It
also associates various managed-code properties with each thread, such
as state, exception handlers, security principal , name, unique ID, and culture (required for localization).
The .NET class Thread, defined in the System.Threading namespace, represents a managed thread. The Thread class provides various methods and properties to control the managed thread.
Calling the methods of the Thread
class (be they static or instance methods) is always done on the stack
of the calling thread, not on the stack of the thread represented by
the Thread object. The one exception to this rule occurs when the calling thread calls methods on a Thread object that represents itself. |
|
You can get hold of the thread on which your code is currently running by using the CurrentThread read-only static property of the Thread class:
public sealed class Thread
{
public static Thread CurrentThread {get;}
//Other methods and properties
}
The CurrentThread property returns an instance of the Thread class. Each thread has a unique thread identification number, called a thread ID. You can access the thread ID via the ManagedThreadId property of the Thread class:
using System.Threading;
Thread currentThread = Thread.CurrentThread;
int threadID = currentThread.ManagedThreadId;
Trace.WriteLine("Thread ID is "+ threadID);
Thread.ManagedThreadId is guaranteed to return a value that is unique process-wide. It's worth mentioning that the thread ID obtained by ManagedThreadId
isn't related to the native thread ID allocated by the underlying
operating system. You can verify that by opening Visual Studio's
Threads debug window (Debug → Windows) during a debugging session and
examining the value in the ID column (see Figure 1). The ID column reflects the physical thread ID. The ManagedThreadId
property simply returns a unique hash of the thread object. Having
different IDs allows .NET threads in different hosting environments
(such as Windows or SQL Server 2005) to map differently to the native
operating support.
Another useful property of the Thread class is the Name string property. Name allows you to assign a human-readable name to a thread:
using System.Threading;
Thread currentThread = Thread.CurrentThread;
string threadName = "Main UI Thread";
currentThread.Name = threadName;
Only
you as the developer can assign the thread name, and by default, a new
.NET thread is nameless. You can only set the thread's name once.
Although naming a thread is optional, I highly recommend doing so
because it's an important productivity feature. Windows doesn't have
the ability to assign a name to a thread. In the past, when you
debugged native Windows code, you had to record the new thread ID in
every debugging session (using the Threads debug window). These IDs
were not only confusing (especially when multiple threads were
involved) but also changed in each new debugging session. Visual
Studio's Threads debug window (see Figure 1) displays the value of the Name
property, thus easing the task of tracing and debugging multithreaded
applications. You can even set a breakpoint filter, instructing the
debugger to break only when a thread with a specific name hits the
breakpoint. Name is a good example of a managed-code property that .NET adds to native threads.
In Visual Studio 2005, breakpoint filters
are disabled by default. To enable them, go to Tools → Options →
Debugging → General and check the "Enable breakpoint filters" checkbox.
Now, you can set a breakpoint filter by selecting Filter...from the
breakpoint context menu. |