See these building blocks for writing applications that respond to user input during process-intensive tasks. Rick Ross of PILLAR Technology Group covers processes, threading, AppDomain, user interface issues and more. (From Delphi for .NET Developer's Guide by Xavier Pacheco, Sams, ISBN: 0-672-32443-1, 2004).
Contributed by Xavier Pacheco Rating: / 9 August 30, 2004
Applications that appear to be nonresponsive are seen as being poorly written. Whether it is waiting for a long-running report to finish, or printing a 100-page document, applications must respond to user input. Fortunately, writing responsive applications is not a difficult task as long as certain principles are understood.
This chapter provides the building blocks for writing applications that respond to user input during process-intensive tasks. In addition, these same concepts are applicable to other applications such as NT Services, Application Servers, and Internet applications.
Processes
A process is created when an application is started. This process contains an instruction pointer that keeps track of the location currently being executed. In addition to executable code, a process contains virtual address space, memory space, and numerous CPU registers.
The virtual address space contains a logical set of valid addresses in a process. Memory space contains the global process data—the stack where local variables are stored, the heap where memory is dynamically allocated, and the set of pages used for mapping virtual addresses to physical memory.
Processes have three unique states: running, stopped, or blocked. Stopped processes are those that are being debugged while blocked processes are waiting for the operating system to execute them. Each process is treated as an isolated entity that is scheduled by the operating system.
Because the operating system prevents processes from directly affecting each other, communication between two or more processes needs a predetermined protocol. Collectively, protocols used to communicate between multiple processes are called Interprocess Communications (IPC). Figure 14.1 illustrates multiple processes communicating with each other.
Figure 14.1 - Multiple processes communicating.
The Windows (NT and above) operating system has several IPC mechanisms to choose from. These include
Named Pipes
Shared Memory
Mutexes
Events
Semaphores
TCP/IP Sockets
Heavyweight is a term associated with processes because they are resource intensive. Starting and stopping a process is relatively slower than other alternatives, which are discussed in the next section. Despite the hunger for increased resources, the level of protection offered by the operating system for processes is unmatched.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
Threading overcomes many disadvantages of using processes to perform background processing. Threading allows for multiple independent paths within a process to be executed simultaneously. Each path of execution is referred to as a thread.
Note - Technically, these independent paths can only run simultaneously on a machine with multiple processors. Single-processor machines switch between these independent paths rapidly, giving the illusion of simultaneous execution.
Most processes only have a single path of execution—a single threaded process. Processes containing multiple paths are called multithreaded. Figure 14.2 illustrates single-threaded and multithreaded processes.
Figure 14.2 - Single and multithreaded processes.
Each thread in a process has its own instruction pointer and CPU registers. All threads share the same virtual address space that is owned by the containing process. However, each thread receives its own stack space.
Because all threads within a process share the same address space, communication between threads is trivial. While easily accomplished, sharing address space requires diligent design. This topic is discussed later in this chapter.
The operating system gives each thread a time slice of the processor. Using a complex algorithm, the operating system looks at the thread's priority and whether it is waiting for something to occur. After a thread has either blocked or used up its time slice, the scheduler looks for another thread to execute.
Lightweight is a term associated with a thread because they require fewer resources than processes. Threads start and stop much more quickly than processes.
Threading .NET Style
As expected, the .NET Framework also has the concept of threading that is nicely wrapped in an object-oriented and platform-neutral fashion. By encapsulating and abstracting threading, the framework exposes a logical thread. These logical threads are managed by the framework and provide additional benefits not found in a physical Win32 thread. Using .NET Threads should make the code much more portable to other CLI platforms such as WinCE, Win64, and Mono. At the time of this writing, a logical thread maps to a physical thread, but this might change in future versions of .NET.
A logical thread is capable of doing things that a native thread cannot. For example, there is no simple method for a native thread to raise an exception in another thread. Logical threads, however, can raise an exception in another thread by calling the Thread.Abort() method. Exceptions are discussed in the "Threading Exceptions" section.
Logical .NET threads are scheduled by the CLR and run in the context of an AppDomain.
AppDomain
Similar to a process, an AppDomain provides a secure sandbox for .NET executables and assemblies. Just as multiple processes are protected, multiple .NET assemblies are protected if they reside in separate AppDomains. One important distinction is that AppDomains do not have to necessarily reside in separate processes. Multiple AppDomains can, and often do, occupy the same Win32 process.
When a .NET application is loaded, the CLR creates a process. In turn, this process creates the first AppDomain, which is called the default AppDomain. The default AppDomain cannot be unloaded and is destroyed when the process finishes. Additional AppDomains can be created dynamically at runtime. The relationship between processes and AppDomains is illustrated in Figure 14.3.
Figure 14.3 - The Process/AppDomain relationship.
Like multiple processes, if an assembly needs to communicate with another assembly located in a different AppDomain, some IPC mechanism is required such as .NET Remoting. Unlike multiple processes, the major distinction between AppDomains and Win32 processes is that AppDomains cannot currently share a common memory segment, whereas Win32 applications can.
AppDomains provide the capability to unloaded assemblies, as long as they are not located in the default AppDomain. All assemblies contained in an AppDomain are unloaded when the AppDomain is unloaded.
Configuration and security policies can be applied to an AppDomain to form either a more restrictive or relaxed environment.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
The .NET Framework has a rich collection of classes and enumerations that are needed for writing multithreaded applications, which are located in the System.Threading namespace.
The System.Threading.Thread Class
Directly inheriting from the System.Object class, the Thread class provides the necessary methods for creating, aborting, suspending, and resuming threads. In addition, several properties exist for controlling the priority and determining other useful information. Listing 14.1 contains a partial definition of the Thread class.
Listing 14.1 Declaration of the System.Threading.Thread Class
System.Threading.Thread = class(System.Object) public constructor Create(start: ThreadStart); procedure Start; // terminate a thread procedure Abort(stateInfo: System.Object); overload; procedure Abort; overload; // cancel an abort request class procedure ResetAbort; static; procedure Suspend; procedure Resume;
// wakes a sleeping thread procedure Interrupt;
// wait for a thread to finish procedure Join; overload; function Join(millisecondsTimeout: integer) : Boolean; overload; function Join(timeout: TimeSpan) : Boolean; overload;
class procedure Sleep(millisecondsTimeout: Integer); overload; static; class procedure Sleep(timeout: TimeSpan); overload; static;
// forces the thread to spin in a loop for a given number of iterations class procedure SpinWait(iterations: Integer); static;
// thread local storage class function AllocateDataSlot: LocalDataStoreSlot; static; class function AllocateNamedDataSlot(name: String) : LocalDataStoreSlot; static; class function GetNamedDataSlot(name: String) : LocalDataStoreSlot; static; class procedure FreeNamedDataSlot(name: String); static; class function GetData(slot: LocalDataStoreSlot) : System.Object; static; class procedure SetData(slot: LocalDataStoreSlot; data: System.Object); static;
class function GetDomain: AppDomain; static; class function GetDomainID: integer; static;
// used with role based security property CurrentPrincipal: System.Security.Principal.IPrincipal read; write;
property Name: System.String read; write; end;
In particular, one property worth noting is the Name property. It can only be written to one time. Any attempt to write to the Name property more than once results in an exception.
Note - Notice that the Thread class contains methods to Suspend() and Resume() threads. Randomly suspending threads is not a good idea because it would be very easy to pause a thread during an inappropriate time. Imagine the results of suspending a thread when a lock is being held or in the middle of some file operation. The bottom line is that only the thread itself should call Suspend() because it knows the best places within the code to pause the thread. Calling Resume() on a suspended thread must be done from another thread.
Creating a thread using the Thread class is accomplished in one of two manners. The most frequent method is to use an instance method of a class. Another alternative is to use a static class method. These two methods of creating threads are referred to as manually created threads.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
Regardless of the method used for manually created threads, both make use of delegates. A delegate is a .NET term for an object-oriented callback method that is type-safe. Similar to event handlers in Delphi, delegates provide a mechanism for multiple objects to be notified when called. Listings 14.2 and 14.3 demonstrate how delegates are used in Delphi when creating a thread.
Listing 14.2 Creating Threads Using Instance Methods
1: program instancethreads; 2: {$APPTYPE CONSOLE} 3: 4: // 5: // This example demonstrates how to use native .NET methods to create a 6: // thread on a class with an instance method. 7: // 8: 9: uses 10: System.Threading; 11: 12: type 13: D4DNInstanceThread = class 14: private 15: FStartNumber : integer; 16: public 17: // ThreadMePlease will be executed on a different thread 18: procedure ThreadMePlease; 19: property StartNumber : integer write FStartNumber; 20: end; 21: 22: procedure D4DNInstanceThread.ThreadMePlease; 23: var 24: stop : integer; 25: curNum : integer; 26: begin 27: curNum := FStartNumber; 28: stop := FStartNumber + 10; 29: while curNum < stop do 30: begin 31: writeln('Thread ', System.Threading.Thread.CurrentThread.Name , 32: ' current value is ',curNum); 33: inc(curNum); 34: Thread.Sleep(3); 35: end; 36: end; 37: 38: var 39: ThreadWork1 : D4DNInstanceThread; 40: ThreadWork2 : D4DNInstanceThread; 41: Thread1 : Thread; 42: Thread2 : Thread; 43: begin 44: writeln('Starting threading instance method example...'); 45: 46: // Create D4DNInstanceThread instance 47: ThreadWork1 := D4DNInstanceThread.Create; 48: ThreadWork1.StartNumber := 10; 49: 50: // Create the thread, specifying the instance method to execute 51: Thread1 := Thread.Create(@ThreadWork1.ThreadMePlease); 52: Thread1.Name := 'one'; 53: 54: // Create another instance of D4DNInstanceThread 55: ThreadWork2 := D4DNInstanceThread.Create; 56: ThreadWork2.StartNumber := 100; 57: 58: // Create the second thread, specifying the instance method to execute 59: Thread2 := Thread.Create(@ThreadWork2.ThreadMePlease); 60: Thread2.Name := 'two'; 61: 62: // Finally start the threads 63: Thread1.Start; 64: Thread2.Start; 65: 66: // Wait for the two threads to finish 67: Thread1.Join; 68: Thread2.Join; 69: 70: // Wait for the user to see the results 71: writeln('Done'); 72: readln; 73: end.
Note: Find the code on the CD: \Code\Chapter 14\Ex01\.
Listing 14.2 demonstrates how to create a thread using an instance method. Any instance method that does not have any parameters can be executed on a thread. Look at the Thread.Create() constructor call (shown in Listing 14.2 on line 51). The parameter to the Thread.Create constructor is the address of the ThreadMePlease method. Under the covers, the Delphi compiler is creating a ThreadStart delegate. Other languages, such as C#, require a few more lines of code to accomplish the same task.
Note - Although the ThreadStart delegate does not allow for passing parameters, using the instance method provides the opportunity to use either the constructor or properties to pass additional information needed by the thread. Listing 14.2 demonstrates this by setting the StartNumber.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
Listing 14.3 Creating Threads Using Static Methods
1: program staticthreads; 2: {$APPTYPE CONSOLE} 3: uses 4: System.Threading; 5: 6: type 7: D4DNStaticThread = class 8: public 9: class procedure ThreadMePlease; static; 10: end; 11: 12: class procedure D4DNStaticThread.ThreadMePlease; 13: var 14: stop : integer; 15: curNum : integer; 16: rnd : System.Random; 17: begin 18: rnd := System.Random.Create; 19: curNum := rnd.Next(1000); 20: stop := curNum + 10; 21: while curNum < stop do 22: begin 23: writeln('Thread ', System.Threading.Thread.CurrentThread.Name, 24: 'current value is ', curNum); 25: inc(curNum); 26: // Randomly give up time-slice to other thread 27: if rnd.Next(100) < 50 then 28: Thread.Sleep(0); 29: end; 30: end; 31: 32: var 33: thrd1 : Thread; 34: thrd2 : Thread; 35: begin 36: Console.Writeline('Starting static method threading example...'); 37: 38: // create the thread passing the static method 39: thrd1 := Thread.Create(@D4DNStaticThread.ThreadMePlease); 40: thrd1.Name := 'one'; 41: 42: // create another identical thread 43: thrd2 := Thread.create(@D4DNStaticThread.ThreadMePlease); 44: thrd2.Name := 'two'; 45: 46: // start both threads 47: thrd1.Start; 48: thrd2.Start; 49: 50: // wait until both threads have finished 51: thrd1.Join; 52: thrd2.Join; 53: 54: Console.Writeline('Done'); 55: readln; 56: end.
Note: Find the code on the CD: \Code\Chapter 14\Ex02\.
In Listing 14.3, a thread is created on a static class method. Similar to threading instance methods, any static class method without parameters can be executed on a thread.
Listings 14.2 and 14.3 contain a subtle bug. The output written to the console is performed in an unsynchronized manner. It is very likely that the output between two threads will be mixed together. This is caused by the implementation of the writeln procedure by the Delphi compiler and the runtime library. Each parameter passed to the writeln procedure results in a separate call to either the Console.Write or Console.WriteLine method. Changing the writeln (on line 31 in Listing 14.2, and line 23 in Listing 14.3) to use Console.WriteLine instead or passing only one parameter to the writeln procedure (for example, using writeln(Format(..));) will produce the proper behavior.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
All manually created threads have an associated priority. This priority allows for boosting or lowering the scheduling of the thread. Table 14.1 lists the enumeration values for ThreadPriority, from the highest to lowest.
Thread is scheduled before any other thread priority.
AboveNormal
Scheduled before any normal priority threads.
Normal
Default setting for threads. Scheduled before BelowNormal threads.
BelowNormal
Scheduled before any threads with lowest priority.
Lowest
Thread is scheduled after all other higher priority threads.
A thread's priority is set using its Priority property. Be careful when changing priorities arbitrarily because that can cause hard to debug conditions such as thread starvation, race conditions, and deadlocks. Never use a higher priority when a lower priority will work.
For GUI applications, most background threads should be set to BelowNormal or Lowest to ensure a responsive user interface no matter how much work the background threads are performing.
System.Threading.ThreadState
Once created, a thread has a state that indicates what the thread is doing. Table 14.2 contains the values of the ThreadState enumeration.
Used internally only, indicates that the thread has been requested to stop executing.
SuspendRequested
The thread has been requested to suspend itself
Background
Controlled by the Thread.IsBackground property, this value indicates that the thread is executing in the background. Background threads are automatically aborted when the main thread terminates. Only foreground threads will prevent an application from exiting as long as they are running.
Unstarted
A thread has been created but not started yet.
Stopped
The thread is no longer executing.
WaitSleepJoin
The thread is not running due to either being blocked, sleeping, or joining (waiting for another thread to finish executing).
Suspended
The thread is suspended.
AbortRequested
The thread has been requested to abort, but has not received the ThreadAbortException yet.
Aborted
Indicates that the thread has been terminated because of Thread.Abort.
Because a thread can be in multiple states, the ThreadState enumeration is a set of bit flags. Not all combinations are valid, however. One valid combination is a thread in a WaitSleepJoin state and an AbortRequested state.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
The .NET Framework creates an apartment when it interacts with COM objects. This apartment is specified in one of two ways. For manually created threads, the ApartmentState property can be set to the appropriate ApartmentState enumeration. These values are listed in Table 14.3.
Table 14.3 Values of the System.Threading.ApartmentState Enumeration
Value
Description
STA
Single-threaded apartment
MTA
Multi-threaded apartment
Unknown
Currently mapped to MTA
Once the ApartmentState property has been set, it cannot be changed again. No error or exception will result from attempting to set this property more than once.
An alternative method is to use either the [STAThread] or [MTAThread] attribute before the first line of code in the project's dpr file. Although it is possible to use the following code to set the apartment model,
using the appropriate attribute guarantees that the apartment state will be set up before any startup code is executed.
The System.Threading.ThreadPool Class
One of the nicest features of threading in the .NET framework is the addition of a thread pooling class. Saving thousands of lines of code, this class provides the means necessary for using threads as easy as calling a single method.
Ideal for short-lived tasks, the ThreadPool class hides the underlying Thread class, taking away the flexibility and control that the Thread class provides.
Many features of the .NET platform use threads from the thread pool. Examples using the thread pool include asynchronous file I/O, timers, socket connections, and the asynchronous execution of delegates.
All AppDomains located within the same process share threads from the same thread pool. Figure 14.3 illustrates this relationship.
Applications request a thread by using one of the QueueUserWorkItem() methods of the ThreadPool class. These methods take a WaitCallback delegate that specifies which method to execute on a thread pool thread. This method is added to an internal queue and is executed when a thread is available.
Be careful when using any of the methods that begin with Unsafe because these methods bypass security checks in order to increase performance.
An example of using threads from the ThreadPool is shown in Listing 14.4.
Listing 14.4 Using ThreadPool Threads
1: program threadpool; 2: {$APPTYPE CONSOLE} 3: uses 4: System.Threading; 5: 6: type 7: TThreadPoolMe = class 8: public 9: // this method will be executed on a threadpool thread 10: procedure ThreadMePlease(state : System.Object); 11: end; 12: 13: procedure TThreadPoolMe.ThreadMePlease(state : System.Object); 14: var 15: id : string; 16: begin 17: id := 'n/a'; 18: if assigned(state) then 19: id := state.ToString; 20: 21: writeln(id,') Hello from the thread pool. Thread ID is ', 22: AppDomain.GetCurrentThreadID,' IsThreadPool = ', 23: Thread.CurrentThread.IsThreadPoolThread); 24: 25: Thread.Sleep(2000); 26: end; 27: 28: const 29: MAX_THREADS = 10; 30: var 31: i : integer; 32: thrd : array[1..MAX_THREADS] of TThreadPoolMe; 33: begin 34: writeln('The main thread''s id is ', AppDomain.GetCurrentThreadID); 35: // queue up a bunch of threads to use the thread pool 36: for i:=1 to MAX_THREADS do 37: begin 38: // create another instance of our class 39: thrd[i] := TThreadPoolMe.Create; 40: writeln('Queueing thread ', i); 41: // now queue up the ThreadMePlease method, passing i in for the state 42: System.Threading.ThreadPool.QueueUserWorkItem( 43: @thrd[i].ThreadMePlease, System.Object(i)); 44: end; 45: // watch the re-use of the thread id's when this application runs 46: readln; 47: writeln('Done'); 48: end.
Note: Find the code on the CD: \Code\Chapter 14\Ex03\.
Listing 14.4 queues 10 threads to the thread pool for execution. Running this example will reveal how the CLR reuses existing threads by examining the reuse of thread IDs.
Note - In addition to manually created threads and threads within the thread pool, the .NET Framework must also keep track of other threads. These threads include the main thread and the finalizer (or the garbage collector) thread. Unmanaged threads can also be running from PInvoke or COM interoperability as well.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
The .NET Framework has three different Timer classes that are used for different purposes. System.Threading.Timer uses threads from the thread pool and instead of firing an event, it uses the specified callback method whose definition is shown below:
TimerCallback = procedure (state: System.Object) of object;
There are four overloaded constructors of the Timer class. All of the constructors are identical except for the type of the last two parameters. The callback parameter is the method to call when the timer fires. Use the state parameter to pass additional information in a System.Object parameter to the callback method. dueTime specifies how long to wait before the callback method is called. This value is specified in milliseconds. Finally, period, also specified in milliseconds, indicates how long to wait between successive calls to the callback method. Use one of the Change() methods to change dueTime or period after creating the Timer instance. Taken from timerthread.dpr, Listing 14.5 demonstrates how to use the Timer class.
Listing 14.5 Declaration of the System.Threading.Timer Class Example
1: program timerthread; 2: {$APPTYPE CONSOLE} 3: uses 4: System.Threading; 5: 6: type 7: TD4DNTimerClass = class 8: public 9: // Alarm will be called when the Timer fires 10: procedure Alarm(state : System.Object); 11: end; 12: 13: procedure TD4DNTimerClass.Alarm(state : System.Object); 14: begin 15: writeln(AppDomain.GetCurrentThreadID,' Bzzzz. Time to wake up!'); 16: if assigned(state) then 17: writeln('You passed me: ', state.ToString); 18: end; 19: 20: var 21: tc : TD4DNTimerClass; 22: t : Timer; 23: begin 24: // create an instance of our class 25: tc := TD4DNTimerClass.Create; 26: // create the timer to call Alarm only once, after delaying 1 second (1000 ms) 27: t := Timer.Create(tc.Alarm, System.Object('Some additional info'), 1000 , 0); 28: writeln(AppDomain.GetCurrentThreadID,' Waiting for the timer to fire.'); 29: // give the timer a chance to fire 30: Thread.Sleep(2000); 31: writeln(AppDomain.GetCurrentThreadID,' Done!'); 32: end.
Note: Find the code on the CD: \Code\Chapter 14\Ex04\.
Delegates
A delegate is a type-safe callback mechanism inherited from System.Delegate. Delegates require a method that is called at the appropriate time. Delegates that descend from System.MulticastDelegate are capable of handling multiple methods. Although delegates are normally called in an synchronous manner using the Invoke() method, the BeginInvoke() method allows for calling delegate methods asynchronously. Only delegates that have one method to call are capable of using the BeginInvoke() method. Unfortunately, the Delphi 8 compiler does not recognize a method pointer as a delegate class instance. Fortunately, BeginInvoke() can still be called by using Reflection. Listing 14.6 demonstrates how to execute a delegate asynchronously.
Listing 14.6 Executing Delegates Asynchronously
1: program AsyncDelegate; 2: {$APPTYPE CONSOLE} 3: uses 4: System.Reflection, 5: System.Threading; 6: 7: type 8: TMyDelegate = procedure of object; 9: 10: TMyClass = class 11: private 12: FOnDoSomething : TMyDelegate; 13: public 14: procedure CallDelegate; 15: property OnDoSomething : TMyDelegate read FOnDoSomething write FOnDoSomething; 16: end; 17: 18: TAnotherClass = class 19: public 20: procedure DoFoo; 21: end; 22: 23: procedure TMyClass.CallDelegate; 24: var 25: obj : System.Object; 26: t : System.Type; 27: m : MethodInfo; 28: parms : array [0..1] of System.Object; 29: begin 30: writeln('CallDelegate'); 31: if Assigned(FOnDoSomething) then 32: begin 33: // writeln('call delegate'); 34: // Normally this would look similar to 35: // @FOnDoSomething.BeginInvoke(nil, nil); 36: // but the compiler doesn't support BeginInvoke, since 37: // it thinks it's just a pointer to a method. 38: // 39: // the work around uses reflection to invoke the method 40: // first cast the "method pointer" to an object 41: obj := System.Object(@FOnDoSomething); 42: // now get the type of the FOnDoSomething 43: t := obj.GetType; 44: // now we gat search for a method named BeginInvoke 45: m := t.GetMethod('BeginInvoke'); 46: // build the parameter list 47: parms[0] := nil; 48: parms[1] := nil; 49: // now we can call BeginInvoke with the parms 50: m.Invoke(obj, parms); 51: end; 52: end; 53: 54: procedure TAnotherClass.DoFoo; 55: begin 56: writeln(AppDomain.GetCurrentThreadID,' DoFoo'); 67: end; 58: 59: var 60: c : TMyClass; 61: ac : TAnotherClass; 62: 63: begin 64: c := TMyClass.Create; 65: ac := TAnotherClass.Create; 66: writeln(AppDomain.GetCurrentThreadID,' assigning the delegate'); 67: c.OnDoSomething := @ac.DoFoo; 68: writeln(AppDomain.GetCurrentThreadID,' calling the delegate'); 69: c.CallDelegate; 70: // give the delegate some time to call it... 71: Thread.Sleep(2000); 72: writeln(AppDomain.GetCurrentThreadID,' Done'); 73: end.
Note: Find the code on the CD: \Code\Chapter 14\Ex05\.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
Writing a multithreaded application is pointless if multiple threads cannot interact in a predictable and bug-free manner. A body of code is said to be thread-safe if multiple threads can safely execute it without any side effects. One way of making a method or function thread-safe is to serialize access to it, thereby allowing only one thread to execute the code at a time.
Thread-safe code can be accomplished by following a few guidelines:
Avoid variables and objects shared between threads. If this is not feasible, use a locking mechanism to serialize access.
Use variables declared on the stack (for example, local variables).
Use stateless routines by passing in all parameters that are needed to do its work.
Use stateless methods, performing work on the internal data of the object. (This assumes that each object instance is only accessible from one thread.) Any additional data should be passed as parameters. Make sure that parameters or fields do not refer to other global data or unprotected objects.
Use thread local storage (threadvar), which is explained in a later section of this chapter.
Fortunately, the .NET Framework provides a rich collection of classes to help write thread-safe code. These classes are generalized as locking and event mechanisms. Locking performs serialization, whereas an event is used for communication between threads.
Locking Mechanisms
Serializing access to a resource is accomplished by using a locking mechanism. By only allowing one thread to enter into a protected region, other threads are locked out. When one thread exits a protected region of code, another thread is then allowed to enter it.
There are three locking mechanisms in the .NET Framework: mutexes, monitors, and Read-Write locks. In addition, the Interlocked class provides a basic set of operations that are atomic. Atomic operations are those that are guaranteed not to be interrupted once they begin.
The System.Threading.WaitHandle Class
Before discussing mutexes and monitors, it is necessary to begin with the WaitHandle class as these two locking mechanisms inherit common functionality from it. Listing 14.7 contains the definition of the WaitHandle class.
Listing 14.7 Declaration of the System.Threading.WaitHandle Class
System.Threading.WaitHandle = class (System.MarshalByRefObject, IDisposable) public constructor Create; procedure Close; virtual; function WaitOne: Boolean; overload; virtual; function WaitOne(timeout: TimeSpan; exitContext: Boolean) : Boolean; overload; virtual; function WaitOne(millisecondsTimeout: Integer; exitContext: Boolean) : Boolean; overload; virtual; class function WaitAll(waitHandles: array of WaitHandle; millisecondsTimeout: Integer; exitContext: Boolean) : Boolean; overload; static; class function WaitAll(waitHandles: array of WaitHandle; timeout: TimeSpan; exitContext: Boolean) : Boolean; overload; static; class function WaitAll(waitHandles: array of WaitHandle) : Boolean; overload; static; class function WaitAny(waitHandles: array of WaitHandle; millisecondsTimeout: Integer; exitContext: Boolean) : Integer; overload; static; class function WaitAny(waitHandles: array of WaitHandle; timeout: TimeSpan; exitContext: Boolean) : Integer; overload; static; class function WaitAny(waitHandles: array of WaitHandle) : Integer; overload; static; property Handle: System.IntPtr read; write; end;
Similar to the Win32 API WaitForMultipleObjects(), the overloaded WaitAll() method accepts multiple WaitHandle objects and will only return if all handles are signaled. The WaitAny() method returns if any of the multiple WaitHandle objects are signaled. Both methods have optional timeout parameters that enable the methods to exit the wait prematurely and avoid deadlocks.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
A mutex is a mutually exclusive object that acts similar to a lock. Locking the mutex is accomplished by calling one of the overloaded WaitOne() methods. Mutexes have the option of being named. Specifying a name allows for the mutex to be shared between AppDomains and processes. The mutex is unlocked by calling the ReleaseMutex() method. Listing 14.8 shows the methods declared in the Mutex class.
Listing 14.8 Declaration of the System.Threading.Mutex Class
System.Threading.Mutex = class (System.Threading.WaitHandle) public constructor Create(initiallyOwned: Boolean; name: String; var createdNew: Boolean); overload; constructor Create(initiallyOwned: Boolean; name: String); overload; constructor Create(initiallyOwned: Boolean); overload; constructor Create; overload; procedure ReleaseMutex; end;
In addition to the Wait methods inherited from WaitHandle, the Mutex class adds several constructors as well as the ReleaseMutex() method. These constructors provide the ability to create, optionally lock (own), and optionally name the mutex. A mutex is unlocked by using the ReleaseMutex() method.
The System.Threading.Monitor Class
The System.Threading.Monitor class has been designed to be more lightweight than the Mutex class. It should be used when a high-performance locking mechanism is needed.
Although the Monitor class looks similar to a mutex, there are some subtle differences. Take a look at Listing 14.9 for the definition of the Monitor class.
Listing 14.9 Declaration of the System.Threading.Monitor Class
System.Threading.Monitor = class (System.Object) public class procedure Enter(obj: System.Object); static; class function TryEnter(obj: System.Object) : Boolean; overload; static; class function TryEnter(obj: System.Object; millisecondsTimeout: Integer) : Boolean; overload; static; class function TryEnter(obj: System.Object; timeout: TimeSpan) : Boolean; overload; static; class function Wait(obj: System.Object; millisecondsTimeout: Integer; exitContext: Boolean) : Boolean; overload; static; class function Wait(obj: System.Object; timeout: TimeSpan; exitContext: Boolean) : Boolean; overload; static; class function Wait(obj: System.Object; millisecondsTimeout: Integer) : Boolean; overload; static; class function Wait(obj: System.Object; timeout: TimeSpan) : Boolean; overload; static; class function Wait(obj: System.Object) : Boolean; overload; static; class procedure Pulse(obj: System.Object); static; class procedure PulseAll(obj: System.Object); static; class procedure Exit(obj: System.Object); static; end;
Using the Enter() and Exit() methods of the Monitor has the same effect as locking with a Mutex's WaitOne() and ReleaseMutex() methods. The Monitor class also has TryEnter() methods that attempt to acquire a lock without waiting. An optional timeout parameter specifies how long to wait on the lock before giving up.
Notice that all the methods of the Monitor class are class methods. This provides the flexibility to specify any object to lock upon.
Finally, the Monitor class allows for signaling and waiting for a signal with the Wait() and Pulse() methods. Both Wait() and Pulse() methods require the Monitor to be locked—that is, surround the Wait() and Pulse() methods with an Enter() / Exit() pair.
Find the code on the CD: \Code\Chapter 14\Ex06\prodcons.dpr for an example that demonstrates a thread-safe queue using the Mutex and Monitor classes.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
The Mutex and Monitor classes provide a locking mechanism that is useful for preventing a section of code from being executed simultaneously by multiple threads. However, there are times when a lock that can distinguish between a reader and a writer can increase performance. This is especially true when there are more readers than writers or if the writer is infrequent with its updates. Fortunately, the ReaderWriterLock class provides this functionality. Listing 14.10 contains the definition of the ReaderWriterLock class.
Listing 14.10 Declaration of the System.Threading.ReaderWriterLock Class
As expected, two types of locks are available. These locks are referred to as a reader lock and a writer lock. Locks are acquired by using the AcquireReaderLock() and AcquireWriterLock() methods, respectively.
A reader lock is acquired only when no writer locks are being held. That way, multiple readers are allowed. However, all readers are blocked when a writer lock is obtained. Only one writer lock is allowed. Find the code on the CD: \Code\Chapter 14\Ex07\TestRWLock.dpr for an example of how to use the ReaderWriterLock class.
The System.Threading.Interlocked Class
Because of the random nature of the way the kernel schedules threads, there is no way to prevent a block of code from being interrupted to execute another thread. Code that needs to be executed atomically—that is, without interruption—requires the use of the System.Threading.Interlocked class. Listing 14.11 contains the class definition of the Interlocked class.
Listing 14.11 Declaration of the System.Threading.Interlocked Class
System.Threading.Interlocked = class (System.Object) class function Increment(var location: Integer) : Integer; overload; static; class function Increment(var location: Int64) : Int64; overload; static;
class function Decrement(var location: Integer) : Integer; overload; static; class function Decrement(var location: Int64) : Int64; overload; static; class function Exchange(var location1: Integer; value: Integer) : Integer; overload; static; class function Exchange(var location1: Single; value: Single) : Single; overload; static; class function Exchange(var location1 : System.Object; value : System.Object) : System.Object; overload; static; class function CompareExchange(var location1: Integer; value: Integer; comparand: Integer) : Integer; overload; static; class function CompareExchange(var location1: Single; value: Single; comparand: Single) : Single; overload; static; class function CompareExchange(var location1 : System.Object; value : System.Object; comparand : System.Object) : System.Object; overload; static; end;
Notice that all methods are class methods, so an instance of this class is never needed. Each distinct operation is guaranteed to finish execution once it begins.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
Although preventing access to a critical block of code is an important task when writing multithreaded applications, there is also the need to be able to signal a waiting thread. It is common to have one or more threads produce work for other threads to manipulate. Threads that create work are referred to as producer threads. Consumer threads are those that do the actual work handed out by the producer threads.
The signal used by the .NET Framework is known as an event. Two types of events are available—AutoResetEvent and ManualResetEvent. When an event is set, it is signaled. Similarly, when an event is reset, the state of the event is not signaled. Events are automatically reset by the .NET Framework when exactly one thread is released when waiting on the event. Manually reset events must be cleared programmatically, and it is likely for multiple threads to be released. Listings 14.12 and 14.13 show the class definition of both types of events.
Listing 14.12 Declaration of the System.Threading.ManualResetEvent Class
System.Threading.ManualResetEvent = class (System.Threading.WaitHandle) public constructor Create(initialState: Boolean); procedure Close; virtual; function WaitOne: Boolean; virtual; overload; function WaitOne(timeout: TimeSpan; exitContext: Boolean) : Boolean; virtual; overload; function WaitOne(millisecondsTimeout: Integer; exitContext: Boolean) : Boolean; virtual; overload; function Reset: Boolean; function Set: Boolean; class function WaitAll(waitHandles: WaitHandle[]; millisecondsTimeout: Integer; exitContext: Boolean) : Boolean; overload; static; class function WaitAll(waitHandles: WaitHandle[]; timeout: TimeSpan; exitContext: Boolean) : Boolean; overload; static; class function WaitAll(waitHandles: WaitHandle[]) : Boolean; overload; static; class function WaitAny(waitHandles: WaitHandle[]; millisecondsTimeout: Integer; exitContext: Boolean) : Integer; overload; static; class function WaitAny(waitHandles: WaitHandle[]; timeout: TimeSpan; exitContext: Boolean) : Integer; overload; class function WaitAny(waitHandles: WaitHandle[]) : Integer; overload; static; property Handle: System.IntPtr read; write; end;
Listing 14.13 Declaration of the System.Threading.AutoResetEvent Class
System.Threading.AutoResetEvent = class (System.Threading.WaitHandle, IDisposable) public constructor Create(initialState: Boolean); function Reset: Boolean; function Set: Boolean; end;
Using either type of event is identical. Use the Set() method to signal an event and the Reset() method to clear the signal. Notice that several methods are used for waiting on an event. The overloaded WaitOne() method waits for only one event, whereas the WaitAll() method waits for every entity to be signaled, and the WaitAny() method allows for waiting for any one signal to be fired. Finally, both the WaitAll() and WaitAny() methods provide the capability of waiting for a Mutex or either type of event. Recall that the WaitAll() and WaitAny() methods are inherited from WaitHandle and are listed in the earlier section "The System.Threading.WaitHandle Class."
Find the code on the CD: \Code\Chapter 14\Ex06\prodcons.dpr for an example demonstrating how to use Events. Be sure to define the USE_EVENTS symbol.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
Using variables that are global in a multithreaded application is a surefire way to cause debugging nightmares. Unless these global variables are protected, sleepless nights are guaranteed.
Sometimes it is necessary to have a variable that is global to a thread. Thread Local Storage is the name given to variables that are global to a specific thread. The easiest way to create this kind of variable is to use the threadvar keyword. Although they are declared like a standard unit global variable, each thread is allocated a separate slot for storing the contents of the variable.
Initialization of threadvars is not allowed. Each thread needs to initialize the variable before using it. An example of a threadvar looks like
threadvar MyThreadVariable: integer; // remember, no initialization
In the .NET Framework, the System.Threading.Thread class has two methods for creating thread local storage. They are the AllocateDataSlot() and the AllocateNamedDataSlot() methods. Finally, use the ThreadStaticAttribute to create a static field unique for each thread. Delphi for .NET currently maps threadvars with the [ThreadStatic] attribute.
Take extra caution when using thread local storage with any mechanism that uses threads from the ThreadPool. Another thread might or might not have already initialized a thread local variable with an unexpected value.
Win32 Interprocess Communications
The Win32 API has more IPC mechanisms available that have not been ported to the managed world. Named pipes and semaphores can be leveraged by using Platform Invoke (PInvoke). This may be the best option when integration is needed with existing Win32 applications. Be wary of performance issues, however, because PInvoke has associated overhead costs. PInvoke is covered in Chapter 16, "Interoperability: COM Interop and the Platform Invocation Service."
Thread-safe .NET Framework Classes and Methods
Thread safety is a serious issue when writing multithreaded applications. So how can one determine if a particular method or class is thread-safe?
When it comes to thread safety in the .NET Framework, one thing is guaranteed: All methods that are public class (static) methods are thread-safe, as long as they only refer to their parameters. This makes sense because class methods cannot refer to any instance data. In general, all other methods are not thread-safe unless the SDK documentation clearly states otherwise.
The Synchronized() Method
Most collection classes have helper properties and methods. A thread-safe queue is obtained by using the following code:
All access to the synchronized queue is now thread-safe. Be aware that a thread-safe collection will be approximately two times slower than an unsynchronized collection.
The IsSynchronized and SyncRoot Properties
Collections that support the Synchronized() method also have a property that indicates if the collection is in fact synchronized. Use the IsSynchronized property to determine if the collection is synchronized.
Another method of making a collection thread-safe is to use a System.Threading.Monitor. Recall that a Monitor requires an object to perform the locking. This object must be the object returned by collection's SyncRoot property.
Be sure to look for the IsSynchronized and SyncRoot properties on other classes within the .NET Framework—specifically, those classes that implement the ICollection interface. For example, the System.Array class is one class that implements these properties as well. Arrays are not thread-safe, and the IsSynchronized returns false. Serialize access to the array by locking on the SyncRoot property of the array.
Caution - Classes that contain the SyncRoot property require due diligence on the part of the developer to ensure that every access to the array is locked before iterating or indexing its members. Nothing within the .NET Framework prevents one thread from properly locking an instance and another from using the same instance, bypassing any locking mechanism. A better alternative is to write a thread-safe class that encapsulates the appropriate storage mechanism and guarantees that all accesses to the internal class are locked.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
In order to optimize performance of the User Interface (UI), both WinForms and VCL applications have not implemented any thread-safety mechanisms. Any updates to a control must occur from the main user-interface thread.
For WinForm applications, most methods of the System.Windows.Forms.Control class are not thread-safe. There are, however, four methods that are safe to execute from any thread. These methods are identified in the following sections. Listing 14.14 contains the methods and properties of interest for this section.
System.Windows.Forms.Control = class ( System.ComponentModel.Component, ISynchronizeInvoke, ...) public ... function Invoke(method: Delegate) : System.Object; overload; function Invoke(method: Delegate; args: array of System.Object) : System.Object; overload; function EndInvoke(asyncResult: IAsyncResult) : System.Object; function BeginInvoke(method: Delegate; args: array of System.Object) : IAsyncResult; overload; function BeginInvoke(method: Delegate) : IAsyncResult; overload; property InvokeRequired: Boolean read; end;
The System.Windows.Forms.Control.Invoke() Method
The most common way of ensuring that a control is updated from the user interface thread is to use one of the overloaded Invoke() methods. Invoke() requires a delegate and an optional array of arguments. When Invoke() is called, it guarantees that the delegate method is executed on the same thread that the control was created on. In addition, Invoke() blocks until the method is executed, thereby causing the worker thread to pause. (Listing 14.15 shows an example of calling the Invoke() method.)
Note: Never call Join() from the main thread that uses the Invoke() method to update the UI thread. This causes the application to hang because the main thread is waiting for the worker thread and the worker thread is waiting for the UI thread.
The System.Windows.Forms.Control.InvokeRequired Property
Suppose that a method will be shared between a user-interface thread and a worker thread. As previously discussed, the worker thread will need to use the Invoke() method to ensure that the update occurs on the proper thread. However, the user-interface thread does not need to use the Invoke() method. Fortunately, the InvokeRequired property provides the flexibility to know if the method can be called directly or if the Invoke() method must be used. Listing 14.15 shows how to use the InvokeRequired property.
Listing 14.15 Example of Invoke() and InvokeRequired
type TMyUpdateDelegate = procedure of object;
procedure TWinForm.UpdateControls; var myDelegate: TMyUpdateDelegate; tmpStr : string; begin if progBar.InvokeRequired then begin tmpStr := System.String.Format('{0} invoke required!', System.Object(AppDomain.GetCurrentThreadID)); UpdateTextBox(System.Object(tmpStr)); myDelegate := @self.UpdateControls; progBar.Invoke(System.Delegate(@myDelegate)); end else begin progBar.Increment(1); trackBar.Value := trackBar.Value + 1; end; end;
Find the code on the CD: \Code\Chapter 14\Ex08\.
Taken from the ThreadingExamples project, the UpdateControls() method is called from the main user-interface thread and other worker threads. If it is called from the worker thread, a delegate is used to call the same method on the main user-interface thread. Notice that two controls are being updated: progBar and trackBar. The only InvokeRequired test is on the progress bar since both controls were created on the same thread.
The System.Windows.Forms.Control.BeginInvoke() Method
When it is not desirable to wait for the main thread to execute, use one of the overloaded BeginInvoke() methods. These methods do the same thing as Invoke() except that they execute the method asynchronously, without waiting. Make sure that the delegate method used is thread-safe. (Listing 14.16 shows an example of using BeginInvoke().)
The System.Windows.Forms.Control.EndInvoke() Method
Executing a method asynchronously is very powerful, especially if a return value is not needed. When a return value is needed from a method executed with BeginInvoke(), use the EndInvoke() method. EndInvoke() returns a System.Object, which represents the return value from the delegate executed asynchronously. Use EndInvoke() carefully because it will block if the method has not completed when EndInvoke() is called. Listing 14.16, which is also from the ThreadingExamples project, illustrates how to use the BeginInvoke() and EndInvoke() methods.
Listing 14.16 BeginInvoke()/EndInvoke() Example
procedure TWinForm.UpdateControlsNoWaiting; var myDelegate: TMyTextBoxDelegate; tmpStr : string; parms : array[0..0] of System.object; lastAsyncInvoke: IAsyncResult;
begin if textBox.InvokeRequired then begin tmpStr := System.String.Format( '{0} Invoke required in UpdateControlsNoWaiting', System.Object(AppDomain.GetCurrentThreadID)); myDelegate := @self.LongRunningMethod; parms[0] := System.Object(tmpStr); lastAsyncInvoke := textBox.BeginInvoke( System.Delegate(@myDelegate), parms ); tmpStr := System.String.Format( '{0} After BeginInvoke call in UpdateControlsNoWaiting', System.Object(AppDomain.GetCurrentThreadID)); UpdateTextBox(System.Object(tmpStr)); // now let's wait for the async call to finish.. textBox.EndInvoke(lastAsyncInvoke); tmpStr := System.String.Format( '{0} After EndInvoke call in UpdateControlsNoWaiting', System.Object(AppDomain.GetCurrentThreadID)); UpdateTextBox(System.Object(tmpStr)); end else UpdateTextBox( 'Invoke is not required in UpdateControlsNoWaiting'); end;
Note: Find the code on the CD: \Code\Chapter 14\Ex08\.
The System.Windows.Forms.Control.CreateGraphics() Method
Access to the GDI+ drawing subsystem is accomplished through the System.Drawing.Graphics class. An instance of this class is available by calling a control's CreateGraphics() method. The CreateGraphics() method is thread-safe, allowing multiple threads to update a GDI+ drawing surface without affecting each other. GDI+ resources are limited, so make sure to use the Dispose() method when finished with the Graphics instance.
One word of caution when using CreateGraphics(). Make sure that any drawing is done within a paint handler. Any updating of a control occurring outside of a paint handler will be erased when the next paint message is processed. Listing 14.17, also from the ThreadingExamples project, demonstrates how two threads can safely paint on a control at the same time. The painting does not occur within a paint handler, so the drawing is easily erased.
Listing 14.17 CreateGraphics() Example
1: procedure TWinForm.btnCreateGraphics_Click(sender: System.Object; e: System.EventArgs); 2: var 3: inst : array[0..1] of TMyGraphicsClass; 4: thrd : Thread; 5: i : integer; 6: begin 7: // this method creates a couple of TMyGraphicClass instances and executes 8: // the draw method on different threads 9: UpdateTextBox('Anything causing a repaint erase the graphics being currently drawn'); 10: for i:=low(inst) to high(inst) do 11: begin 12: inst[i] := TMyGraphicsClass.Create; 13: inst[i].Control := Self.Panel1; 14: inst[i].DrawRectangle := (i = 0); 15: 16: thrd := Thread.Create(@inst[i].Draw); 17: thrd.Start; 18: end; 19: end; 20: 21: procedure TMyGraphicsClass.Draw; 22: var 23: gr : System.Drawing.Graphics; 24: clr : Color; 25: i : integer; 26: height : integer; 27: width : integer; 28: rnd : System.Random; 29: curX : integer; 30: curY : integer; 31: curW : integer; 32: curH : integer; 33: thrdID : System.Object; 34: tmpStr : string; 35: begin 36: // this method randomly draws either Red circles or Blue Rectangles 37: thrdID := System.Object(AppDomain.GetCurrentThreadId); 38: tmpStr := System.String.Format('{0} Graphics Class Thread Started',thrdId); 39: theWinFormInstance.UpdateTextBox(System.Object(tmpStr)); 40: 41: try 42: clr := Color.Red; 43: if FDrawRectangle then 44: clr := Color.Blue; 45: 46: gr := FControl.CreateGraphics; 47: try 48: rnd := System.Random.Create; 49: 50: height := System.Convert.ToInt32(gr.VisibleClipBounds.Height); 51: width := System.Convert.ToInt32(gr.VisibleClipBounds.Width); 52: 53: for i:=1 to 10 do 54: begin 55: curX := rnd.Next(width); 56: curY := rnd.Next(height); 57: curW := rnd.Next(width div 3); 58: curH := rnd.Next(height div 3); 59: 60: if (curX + curW) > width then 61: curX := width - curW; 62: 63: if (curY + curH) > height then 64: curY := height - curH; 65: 66: if FDrawRectangle then 67: gr.FillRectangle(SolidBrush.Create(clr), curX, curY, curW, curH) 68: else 69: gr.FillEllipse(SolidBrush.Create(clr), curX, curY, curW, curH); 70: 71: Thread.Sleep(2000); 72: end; 73: finally 74: gr.Dispose; 75: end; 76: except 77: on e : exception do 78: begin 79: thrdID := System.Object(AppDomain.GetCurrentThreadId); 80: tmpStr := System.String.Format('{0} Graphics class thread Exception {1}', thrdID, 81: System.Object(e.message)); 82: theWinFormInstance.UpdateTextBox(System.Object(tmpStr)); 83: end; 84: end; 85: 86: thrdID := System.Object(AppDomain.GetCurrentThreadId); 87: tmpStr := System.String.Format('{0} Graphics Class Thread Finished',thrdId); 88: theWinFormInstance.UpdateTextBox(System.Object(tmpStr)); 89: end;
Note: Find the code on the CD: \Code\Chapter 14\Ex08\.
Listing 14.17 creates two threads that draw either a rectangle or an ellipse. Before the Draw() method can paint on the control, it must first grab an instance to the Graphics class, which is illustrated on line 46. Once this instance is obtained, each thread is free to update the same control simultaneously.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
Exceptions need to be dealt with appropriately in order to avoid threads terminating prematurely. Any exception within a thread that is not handled will cause the thread to be terminated. Therefore, it is a good programming practice to at least report all exceptions and handle those exceptions that are expected.
System.Threading.ThreadAbortException
Calling the Thread.Abort() method will raise the ThreadAbortException on the target thread. Normally, this will cause the thread to terminate. However, a thread can catch the ThreadAbortException and choose to ignore it by using the Thread.ResetAbort() method.
There are two overloaded Abort() methods. One method does not take any parameters; the other takes a System.Object that provides state information to the thread. This parameter is examined by looking at the ExceptionState property of the ThreadAbortException. Listing 14.18 demonstrates how to handle the ThreadAbortException, taken from ThreadingExceptions.dpr.
Listing 14.18 ThreadingExceptions Example
1: program ThreadingExceptions; 2: {$APPTYPE CONSOLE} 3: 4: // 5: // This example demonstrates ThreadingExceptions. 6: // - ThreadAbortException is handled, and reset 7: // - ThreadInterruptedException is handled 8: // - ThreadStateException is handled 9: // - SynchronizationLockException is handled 10: // 11: 12: uses 13: System.Threading; 14: 15: type 16: D4DNThreadMe = class 17: public 18: procedure MyThreadMethod; 19: end; 20: 21: procedure D4DNThreadMe.MyThreadMethod; 22: var 23: numAborts : integer; 24: 25: begin 26: numAborts := 0; 27: while true do 28: begin 29: try 30: write('*'); 31: Thread.Sleep(1000); 32: except 33: on e : System.Threading.ThreadInterruptedException do 34: begin 35: writeln('Handled ThreadInterruptedException'); 36: end; 37: on e : System.Threading.ThreadAbortException do 38: begin 39: writeln('Handled threadAbortException'); 40: inc(numAborts); 41: if numAborts = 1 then 42: Thread.ResetAbort; 43: end; 44: on e : exception do 45: begin 46: writeln('Unhandled exception ',e.message); 47: raise; 48: end; 49: end; 50: end; 51: end; 52: 53: var 54: thrdclass1 : D4DNThreadMe; 55: thrd1 : Thread; 56: cmd : string; 57: bDone : boolean; 58: 59: begin 60: System.Console.WriteLine('Staring threading exceptions example...'); 61: 62: // create myDotNetThread instance 63: thrdclass1 := D4DNThreadMe.create; 64: thrd1 := Thread.Create(@thrdclass1.MyThreadMethod); 65: thrd1.Start; 66: 67: bDone := false; 68: while not bDone do 69: begin 70: System.Console.WriteLine('Enter A = Abort, I = Interrupt, ' + 71: 'S = ThreadStateException, L = SyncLockException'); 72: cmd := System.Console.ReadLine.ToUpper; 73: if (cmd = 'A') then 74: thrd1.Abort // raise a ThreadAbortException 75: else if (cmd = 'I') then 76: thrd1.Interrupt // raise a ThreadInterruptedException 77: else if (cmd = 'S') then 78: begin 79: try 80: thrd1.Start 81: except 82: on e : System.Threading.ThreadStateException do 83: begin 84: System.Console.WriteLine('Handled ThreadStateException'); 85: end; 86: end; 87: end 88: else if (cmd = 'L') then 89: begin 90: try 91: // needs to be wrapped in an Enter/Exit() block 92: System.Threading.Monitor.Wait(thrd1); 93: except 94: on e : System.Threading.SynchronizationLockException do 95: begin 96: System.Console.WriteLine('Handled SynchronizationLockException'); 97: end; 98: end; 99: end 100: else 101: bDone := true; 102: end; 103: 104: // ensure that the thread is terminated 105: thrd1.Abort; 106: Thread.Sleep(1000); 107: thrd1.Abort; 108: 109: System.Console.WriteLine('Done - main'); 110: end.
Note: Find the code on the CD: \Code\Chapter 14\Ex09\.
System.Threading.ThreadInterruptedException
Threads that are in a WaitSleepJoin state can be interrupted by using the Thread.Interrupt() method. If a thread is not in the WaitSleepJoin state, the exception will fire the next time that it goes into that state. Unhandled, this exception will terminate the thread. Listing 14.18 shows an example of the ThreadInterrupedException.
System.Threading.ThreadStateException
Suppose that an application has two threads—A and B. If thread A tries to coerce thread B into an invalid state, a ThreadStateException will be raised in thread A. Trying to transition from a running state to a restarted state or from a terminated state to a restarted state are both illegal state transitions that will trigger the ThreadStateException. Listing 14.18 contains an example of how the ThreadStateException is raised and handled.
System.Threading.SynchronizationLockException
Attempting to use certain methods of the Monitor class incorrectly will raise the SynchronizationLockException. For example, calling the Wait() method outside of a pair of Enter()/Exit() methods triggers this exception. Listing 14.18 illustrates an example.
Garbage Collection and Threading
Garbage collection can occur at any time. In order to perform its job, all threads are suspended. If any managed threads are currently executing unmanaged code, these threads are resumed. However, before resuming these threads, the CLR inserts code to suspend the thread when it returns to managed code. These threads are allowed to resume because memory is pinned (locked down) when it is referenced by unmanaged code.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.