(Post 05/10/2007) 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:
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.
Passing Data to ThreadStart
Let’s say, in the example above, we wanted to better
distinguish the output from each thread, perhaps by having one of the
threads write in upper case. We could achieve this by passing a flag to
the Go method: but then
we couldn’t use the ThreadStart
delegate because it doesn’t accept arguments. Fortunately, the .NET framework
defines another version of the delegate called ParameterizedThreadStart,
which accepts a single object argument as follows:
public delegate void ParameterizedThreadStart
(object obj);
The previous example then looks like this:
class ThreadTest {
static void Main() {
Thread t = new Thread (Go);
t.Start (true); // == Go (true)
Go (false);
}
static void Go (object upperCase) {
bool upper = (bool) upperCase;
Console.WriteLine (upper ? "HELLO!" : "hello!");
}
In this example, the compiler automatically infers a
ParameterizedThreadStart
delegate because the Go method accepts a single object argument. We could
just as well have written:
Thread t = new Thread (new
ParameterizedThreadStart (Go));
t.Start (true);
A feature of using ParameterizedThreadStart
is that we must cast the object
argument to the desired type (in this case bool)
before use. Also, there is only a single-argument version of this delegate.
An alternative is to use an anonymous method to call
an ordinary method as follows:
static void Main() {
Thread t = new Thread (delegate() { WriteText ("Hello"); });
t.Start();
}
static void WriteText (string text) { Console.WriteLine (text); }
The advantage is that the target method (in this case
WriteText) can accept any
number of arguments, and no casting is required. However one must take
into account the outer-variable semantics of anonymous methods, as is
apparent in the following example:
static void Main() {
string text = "Before";
Thread t = new Thread (delegate() { WriteText (text); });
text = "After";
t.Start();
}
static void WriteText (string text) { Console.WriteLine (text); }
Anonymous methods open
the grotesque possibility of unintended interaction via outer
variables if they are modified by either party subsequent to the
thread starting. Intended interaction (usually via fields) is
generally considered more than enough! Outer variables are best
treated as ready-only once thread execution has begun – unless
one's willing to implement appropriate locking semantics on both
sides. |
Another common system for passing data to a thread is
by giving Thread an instance method rather than a static method. The instance
object’s properties can then tell the thread what to do, as in the following
rewrite of the original example:
class ThreadTest {
bool upper;
static void Main() {
ThreadTest instance1 = new ThreadTest();
instance1.upper = true;
Thread t = new Thread (instance1.Go);
t.Start();
ThreadTest instance2 = new ThreadTest();
instance2.Go(); // Main thread – runs with upper=false
}
void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!");
}
Naming Threads
A thread can be named via its Name
property. This is of great benefit in debugging: as well as being able
to Console.WriteLine a thread’s
name, Microsoft Visual Studio picks up a thread’s name and displays it
in the Debug Location toolbar. A thread’s name can be set at any time
– but only once – attempts to subsequently change it will throw an exception.
The application’s main thread can also be assigned a
name – in the following example the main thread is accessed via the CurrentThread
static property:
class ThreadNaming {
static void Main() {
Thread.CurrentThread.Name = "main";
Thread worker = new Thread (Go);
worker.Name = "worker";
worker.Start();
Go();
}
static void Go() {
Console.WriteLine ("Hello from " + Thread.CurrentThread.Name);
}
}
Foreground and Background
Threads
By default, threads are foreground threads, meaning they
keep the application alive for as long as any one of them is running.
C# also supports background threads, which don’t keep the application
alive on their own – terminating immediately once all foreground threads
have ended.
Changing a thread from
foreground to background doesn’t change its priority or status
within the CPU scheduler in any way. |
A thread's IsBackground
property controls its background status, as in the following example:
class PriorityTest {
static void Main (string[] args) {
Thread worker = new Thread (delegate() { Console.ReadLine(); });
if (args.Length > 0) worker.IsBackground = true;
worker.Start();
}
}
If the program is called with no arguments, the worker
thread runs in its default foreground mode, and will wait on the ReadLine
statement, waiting for the user to hit Enter.
Meanwhile, the main thread exits, but the application keeps running because
a foreground thread is still alive.
If on the other hand an argument is passed to Main(),
the worker is assigned background status, and the program exits almost
immediately as the main thread ends – terminating the ReadLine.
When a background thread terminates in this manner, any
finally blocks are circumvented.
As circumventing finally
code is generally undesirable, it's good practice to explicitly wait for
any background worker threads to finish before exiting an application
– perhaps with a timeout (this is achieved by calling Thread.Join).
If for some reason a renegade worker thread never finishes, one can then
attempt to abort it, and
if that fails, abandon the thread, allowing it to die
with the process (logging the conundrum at this stage
would also make sense!)
Having worker threads as background threads can then
beneficial, for the very reason that it's always possible to have the
last say when it comes to ending the application. Consider the alternative
– foreground thread that won't die – preventing the application from exiting.
An abandoned foreground worker thread is particularly insidious with a
Windows Forms application, because the application will appear to exit
when the main thread ends (at least to the user) but its process will
remain running. In the Windows Task Manager, it will have disappeared
from the Applications tab, although its executable filename still be visible
in the Processes tab. Unless the user explicitly locates and ends the
task, it will continue to consume resources and perhaps prevent a new
instance of the application from starting or functioning properly.
A common cause for an
application failing to exit properly is the presence of “forgotten”
foregrounds threads. |
Thread Priority
A thread’s Priority
property determines how much execution time it gets relative to other
active threads in the same process, on the following scale:
enum ThreadPriority { Lowest,
BelowNormal, Normal, AboveNormal, Highest }
This becomes relevant only when multiple threads are
simultaneously active.
Setting a thread’s priority to high doesn’t mean it can
perform real-time work, because it’s still limited by the application’s
process priority. To perform real-time work, the Process
class in System.Diagnostics
must also be used to elevate the process priority as follows (I didn't
tell you how to do this):
Process.GetCurrentProcess().PriorityClass
= ProcessPriorityClass.High;
ProcessPriorityClass.High
is actually one notch short of the highest process priority: Realtime.
Setting one's process priority to Realtime
instructs the operating system that you never want your process to be
preempted. If your program enters an accidental infinite loop you can
expect even the operating system to be locked out. Nothing short of the
power button will rescue you! For this reason, High
is generally considered the highest usable process priority.
If the real-time application has a user interface, it
can be undesirable to elevate the process priority because screen updates
will be given excessive CPU time – slowing the entire computer, particularly
if the UI is complex. (Although at the time of writing, the Internet telephony
program Skype gets away with doing just this, perhaps because its UI is
fairly simple). Lowering the main thread’s priority – in conjunction with
raising the process’s priority – ensures the real-time thread doesn’t
get preempted by screen redraws, but doesn’t prevent the computer from
slowing, because the operating system will still allocate excessive CPU
to the process as a whole. The ideal solution is to have the real-time
work and user interface in separate processes (with different priorities),
communicating via Remoting or shared memory. Shared memory requires P/Invoking
the Win32 API (web-search CreateFileMapping and MapViewOfFile).
Exception Handling
Any try/catch/finally
blocks in scope when a thread is created are of no relevance once the
thread starts executing. Consider the following program:
public static void Main() {
try {
new Thread (Go).Start();
}
catch (Exception ex) {
// We'll never get here!
Console.WriteLine ("Exception!");
}
static void Go() { throw null; }
}
The try/catch statement in this example is effectively
useless, and the newly created thread will be encumbered with an unhandled
NullReferenceException.
This behavior makes sense when you consider a thread has an independent
execution path. The remedy is for thread entry methods to have their own
exception handlers:
public static void Main() {
new Thread (Go).Start();
}
static void Go() {
try {
...
throw null; // this exception will get caught below
...
}
catch (Exception ex) {
Typically log the exception, and/or signal another thread
that we've come unstuck
...
}
From .NET 2.0 onwards, an unhandled exception on any
thread shuts down the whole application, meaning ignoring the exception
is generally not an option. Hence a try/catch
block is required in every thread entry method – at least in production
applications – in order to avoid unwanted application shutdown in case
of an unhandled exception. This can be somewhat cumbersome – particularly
for Windows Forms programmers, who commonly use the "global"
exception handler, as follows:
using System;
using System.Threading;
using System.Windows.Forms;
static class Program {
static void Main() {
Application.ThreadException += HandleError;
Application.Run (new MainForm());
}
static void HandleError (object sender, ThreadExceptionEventArgs e) {
Log exception, then either exit the app or continue...
}
}
The Application.ThreadException
event fires when an exception is thrown from code that was ultimately
called as a result of a Windows message (for example, a keyboard, mouse
or "paint" message) – in short, nearly all code in a typical
Windows Forms application. While this works perfectly, it lulls one into
a false sense of security – that all exceptions will be caught by the
central exception handler. Exceptions thrown on worker threads are a good
example of exceptions not caught by Application.ThreadException
(the code inside the Main
method is another – including the main form's constructor, which executes
before the Windows message loop begins).
The .NET framework provides a lower-level event for global
exception handling: AppDomain.UnhandledException.
This event fires when there's an unhandled exception in any thread, and
in any type of application (with or without a user interface). However,
while it offers a good last-resort mechanism for logging untrapped exceptions,
it provides no means of preventing the application from shutting down
– and no means to suppress the .NET unhandled exception dialog.
In production applications,
explicit exception handling is required on all thread entry methods.
One can cut the work by using a wrapper or helper class to perform
the job, such as BackgroundWorker
(discussed in Part 3). |
(Sưu tầm) |