Threads and .NET

I l @ ve RuBoard

The Windows SDK makes threads available through a series of APIs such as CreateThread . These APIs form a relatively low-level interface, and you're expected to supply information about security, stack usage, and so on. (Some of this information might be blank if you want to use default values, but you still have to specify that it is blank !) Also, because the Windows SDK is based on C and C++, a lot of routine tasks , such as error trapping, are left to the developer. (Error handling is vitally important when you program with the Windows SDK but is too easily forgotten.)

The following sample, WinThread.cpp, is a C++ program that uses CreateThread to create and run a thread that executes the ThreadFunc function, passing it a parameter (variable dwThrdParam ). The ThreadFunc procedure simply displays the value of this parameter in a message box. If you want, you can create a C++ Win32 project using Microsoft Visual Studio .NET (when the Win32 Application Wizard appears, click Application Settings, and set the Application type to Console application) and type this code (or simply use the sample code in the WinThread project) ”it will compile and run. Notice that the function to be executed by the thread is passed to CreateThread as a pointer, as is the parameter, which is propagated through to ThreadFunc .

WinThread.cpp
 #include "stdafx.h" 
 #include <windows.h> #include <conio.h> DWORD WINAPI ThreadFunc(LPVOID) ; int _tmain(int argc, _TCHAR* argv[]) { DWORD dwThreadId, dwThrdParam = 1; HANDLE hThread; char szMsg[80]; hThread = CreateThread( NULL, // no security attributes 0, // use default stack size ThreadFunc, // thread function &dwThrdParam, // argument to thread function 0, // use default creation flags &dwThreadId); // returns the thread identifier // Check the return value for success. if (hThread == NULL) { wsprintf( szMsg, "CreateThread failed." ); MessageBox( NULL, szMsg, "main", MB_OK ); } else { _getch(); CloseHandle( hThread ); } return 0; } DWORD WINAPI ThreadFunc( LPVOID lpParam ) { char szMsg[80]; wsprintf( szMsg, "Parameter = %d.", *(DWORD*)lpParam ); MessageBox( NULL, szMsg, "ThreadFunc", MB_OK ); return 0; } 

There's plenty of room here for mayhem ”for example, passing pointers to functions that don't match the signature required by CreateThread , or even passing an uninitialized pointer.

The model used by this API permits the method executed by the thread to take a single LPVOID parameter, although this parameter can actually contain almost any type of data. ( LPVOID is C++-speak for "pointer to a generic blob of memory.") The thread function takes it on trust that the parameter has been populated with data of a type that it is expecting.

In the System.Threading namespace of the .NET Framework Class Library, Microsoft has implemented a convenient object-oriented abstraction for creating and managing Windows threads. The .NET Framework Class Library also offers automatic and manual synchronization mechanisms that allow you to control threads effectively. The threads that are created in this way run in managed space inside the common language runtime, which means that they can't maliciously or accidentally interfere with other unrelated threads that execute in other processes. However, threads can also originate from outside of the common language runtime and enter the managed environment, and managed threads fashioned by the common language runtime can call out to the unmanaged space (as discussed later in this and other chapters). Note that when you execute outside the bounds of the common language runtime, a thread is not protected in quite the same way.

Application Domains and Threads

Managed applications being executed by the common language runtime run in application domains. You'll recall from Chapter 2 that an application domain is the unit of isolation used by the common language runtime. Code that runs in one application domain cannot interact directly with code running in another application domain. A single real process that hosts the common language runtime might contain several application domains, each running a different application. One reason for this architecture is speed ”several applications can run inside the same process, so the common language runtime can quickly switch from one application to another.

In execution environments prior to .NET, a thread belongs to a single application running in a single process; a thread running in one application cannot directly access a thread running in another. Instead, threads can make remote procedure calls, which involves marshaling parameters and any return values across application boundaries, and performing a process context switch, which is potentially quite expensive. In the common language runtime the situation is subtly different; threads belong to processes that host application domains. A single thread can span multiple application domains residing in the same process. This means that the same thread can be used to execute different applications at different times. In other words, there is no simple one-to-one relationship between application domains and threads; a single thread can cross application domain boundaries in the same host process, and a single application domain can use many threads. The common language runtime tracks which threads are currently executing in which application domains, and it manages and secures them accordingly .

Tip

You can obtain a reference to the current application domain the current thread is executing in using the static method System.Threading.Thread.GetDomain() .


Creating Threads

The System.Threading namespace in the .NET Framework Class Library contains the Thread class and the ThreadStart delegate. The Thread class represents a thread of execution. As mentioned in Chapter 3, the class System.Threading.Thread performs functions similar to those of the java.lang.Thread class in the JDK, and the two can interoperate to some limited extent. The ThreadStart delegate is used to specify the name of a method to execute when a thread is created and starts running. Again, for purposes of comparison with the JDK, you can think of the method referred to by the ThreadStart delegate as somewhat functionally equivalent to the run method in the java.lang.Runnable interface or java.lang.Thread class.

To create and run a thread, you must create a ThreadStart delegate that refers to a method to be executed by the thread, create a Thread object using this delegate, and then start the thread object running. The ThreadStart delegate must refer to a void method that takes no parameters. In the sample CLRThread.jsl file in the CLRThreads project, the class CLRThread contains a single method called ThreadProc that displays the message" "Thread executing." The main method of the ThreadRunner class creates an instance of CLRThread called threadObject and instantiates a ThreadStart delegate called startProc , which refers to the ThreadProc method of threadObject :

 CLRThread threadObject = new CLRThread(); ThreadStart startProc = new ThreadStart(threadObject.ThreadProc); 

Next, a Thread object is created that will be used to run the ThreadProc method:

 System.Threading.Thread runner = new System.Threading.Thread(startProc); 

Note

Notice the use of System.Threading.Thread . An unqualified Thread reference will refer to a java.lang.Thread . See Chapter 3 for an explanation.


Although the thread exists, it will not run until the Start method is invoked:

 runner.Start(); 

At this point, the ThreadProc method runs. When ThreadProc finishes, the thread terminates.

Threads are asynchronous with respect to the code that creates them. If you want to wait for a thread to finish, you should invoke its Join method:

 runner.Join(); 

If you compile and run the CLRThreads program, you should see output similar to that shown in Figure 8-1.

Figure 8-1. The output of the CLRThreads program

Note that after you call Start to set a thread running, you cannot guarantee exactly when it will execute. In certain circumstances, the "Thread executing" message might appear before the "Thread running" message because of the way threads are scheduled (as explained later in this chapter).

Threads and Security

Multithreading is a powerful tool, but it can also be abused. It is not uncommon for a virus to spawn thousands of threads when it infects a host during a denial of service attack. For this reason, the Start method of the Thread class, along with many of the other methods documented in this chapter, requires that the application run with the ControlThread privilege. If this privilege has not been granted, the common language runtime will throw a SecurityException .

The ControlThread privilege is one of the flags that comprise the Security permission (described briefly in Chapter 2). By default, this privilege is granted only to assemblies originating from the My Computer zone. If you download an assembly from the Internet or your local intranet, or even from a shared drive over the local area network, you'll find that it cannot create or manage threads. However, you can change the code access security using the Microsoft .NET Framework Configuration tool, as described in Chapter 2. (But be careful!)

Passing Parameters to Threads

The first example in this chapter, WinThread.cpp, used a thread to execute a method that took a parameter. The model used by common language runtime threads does not permit you to point a ThreadStart delegate at a method that takes any parameters or returns a value, but you can achieve a similar result using a property). The sample project CLRThreadParam exposes a private int field in the CLRThreadWithParam class using the get_Num and set_Num property accessor methods.

If you compile and run this program, you'll see the message "Thread running" printed by the ThreadRunner class, as well as a message box displaying the value of num output by the thread itself, as shown in Figure 8-2.

Figure 8-2. The output of the ClrThreadParam program

A word of warning: Although this solution is simple and does its job, it is not deterministic. Consider what happens if you change the value of threadObject.num after invoking the Start method:

 System.Threading.Thread runner = new System.Threading.Thread(startProc); runner.Start(); threadObject.set_Num(99); Console.WriteLine("Thread running"); runner.Join(); 

Will the thread display the value 1 (the value that num had when the Start method was invoked) or 99? The answer depends on when the thread was scheduled, and the results can vary. If data can be accessed by more than one thread, you must ensure that any such updates are controlled and will not result in nondeterministic behavior or worse . We'll look at how to do this shortly when we discuss synchronization.

Thread States

A thread can be in one of several states at various stages in its lifecycle. When a thread is first created, it is in the Unstarted state. Once the Start method has been called, the thread moves into the Running state and can execute. When the thread has completed execution, it enters the Stopped state. Note that although the thread might have stopped running, the thread object itself still exists.

We saw earlier that you can cause a thread to wait for another thread to complete by using the Join method. In this case, the calling thread enters the WaitSleepJoin state. The following example causes the current thread to wait until the runner thread finishes:

 System.Threading.Thread runner = new System.Threading.Thread(...); runner.Start(); runner.Join(); 

If a thread joins another thread that has already stopped, the join operation completes immediately and the thread reverts to the Running state. You can determine the current state of a thread using the get_ThreadState method, as shown here. ( ThreadState is a property.)

 System.Threading.Thread runner = new System.Threading.Thread(...); Console.WriteLine("State is " + runner.get_ThreadState()); 

The value returned by get_ThreadState is a member of the ThreadState enumeration and can be one of the following values: Aborted , AbortRequested , Running , Stopped , Suspended , SuspendRequested , Unstarted , or WaitSleepJoin .

Threads can be temporarily halted for a number of reasons, apart from waiting on a call to Join . For example, a thread that is performing an IO operation might be halted while waiting for data to arrive . You can explicitly suspend a thread by calling the Suspend method on that thread:

 System.Threading.Thread runner = new System.Threading.Thread(...); runner.Start(); runner.Suspend(); 

In this example, the runner thread is put into the SuspendRequested state but will continue executing until it reaches a safe point, whereupon it will suspend itself, entering the Suspended state. Calls to the Suspend method do not nest. Suspending a thread that is already in the SuspendRequest or Suspended state will have no effect.

You can resurrect a suspended thread using the Resume method:

 runner.Resume(); 

You can resume only a suspended thread. If the target thread is not in the Suspended state, you'll trigger a ThreadStateException in the calling thread. (You cannot restart a Stopped thread, for example.)

Safe Points

A safe point is a point at which it is safe for the common language runtime to perform garbage collection. A safe point frequently occurs at the end of a method, when execution returns to the calling method. The common language runtime can intercept the flow of control by replacing the return address of the calling method with a different address that allows the runtime to take over and invoke the garbage collector or to perform any other runtime tasks required (such as suspending the thread). When these tasks have completed, the runtime returns control to the calling method and execution continues.

This interception and adjustment of the return address of a method on the stack is performed dynamically whenever the need arises. However, in some situations a method might execute for a long time or maybe even fail to terminate due to a logic error on the part of the programmer (an infinite loop, perhaps). The JIT compiler will detect such a situation before running the program. The JIT compiler will insert code to interrupt such loops if provoked by the need to perform garbage collection or suspend the thread. However, when the thread resumes so will the loop!

A thread can temporarily halt itself for a period of time with the Sleep method. This is a static method belonging to the System.Threading.Thread class. Executing Sleep causes the thread to enter the WaitSleepJoin state. You can specify the time to sleep as a number in milliseconds or as a time span using a System.Timespan argument. For example, to put the current thread to sleep for 2 days, 3 hours, 10 minutes, and 38 seconds, you can use the following:

 TimeSpan ts = new TimeSpan(2, 3, 10, 38); System.Threading.Thread.Sleep(ts); 

Be aware that the parameter of the Sleep method actually specifies the minimum amount of time to suspend the process ”it might actually sleep for longer depending on the load on the machine and the relative priorities of various threads (as discussed later). If you need to sleep for an indefinite period, you can specify the value Timeout.Infinite as the parameter of Sleep . You can execute Sleep with a very short timespan (or even 0) to voluntarily surrender the CPU and indicate that the scheduler should allow any other waiting threads to execute. This is also one way to manually insert a safe point in your code.

Terminating Threads

A thread terminates naturally when it finishes executing the method indicated by the ThreadStart delegate. Also, if a thread triggers an exception that it fails to catch, the exception will be propagated to the runtime which will kill the thread.

Interrupting a Thread

If a thread is in the WaitSleepJoin state (it is waiting for something to happen), you can wake it up, or even terminate it, by calling the Interrupt method:

 System.Threading.Thread runner = new System.Threading.Thread(...); runner.Interrupt(); 

If the target thread ( runner ) is asleep, the Interrupt method will throw a ThreadInterruptException and rouse the thread. (The thread will enter the Running state.) If the thread catches this exception, it can carry on processing. If not, the thread will terminate. If the target thread is not in the WaitSleepJoin state when the Interrupt method is invoked, the interrupt will remain pending until the thread moves into this state, at which point the interrupt will occur immediately. Although interrupting a thread is a good way to wake a sleeping thread, you should not rely on this approach to synchronize operations between threads because you can never be quite sure when the target thread will be interrupted ! Later in this chapter, we'll look at better ways to synchronize threads.

Aborting a Thread

Another way to kill a thread is to use the Abort method:

 System.Threading.Thread runner = new System.Threading.Thread(...); runner.Abort(); 

The Abort method triggers a ThreadAbortException in the target thread, which enters the AbortRequested state. If you don't catch this exception in your thread, it will terminate. If you do catch this exception, the exception handler will run but the thread will still terminate, propagating the exception to any outer blocks in the same manner as an unhandled exception. You should use the exception handler to roll back any changes that need to be undone and to release any resources acquired by the thread. When processing has completed, the thread will be placed in the Stopped state. If you really don't want the thread to die, you can execute the static System.Threading.Thread.ResetAbort() method:

 catch (System.Threading.ThreadAbortException e) { if (...) { System.Threading.Thread.ResetAbort(); } } 

The Aborted State

A race condition might occur if you start a thread and then abort it almost immediately. If the thread has not had a chance to run, you will not have entered any try blocks, so no exception handler will be active (you will not catch the ThreadAbortException ) and no finally code will be executed. However, in this case the state of the thread is set to Aborted . To see what this means, look at the ThreadStates.jsl file in the ThreadStates project.

The ThreadRunner class creates a thread that executes the ThreadFunc method in a CLRStateThread object. This method sleeps for an infinite period of time. After the thread has been started, the Sleep method call temporarily suspends the thread that's running the main method and allows the runner thread to obtain the processor and execute. When the main method resumes, it aborts the runner thread and then waits for it to terminate before printing out its state. On a single processor computer, we obtained the output shown in Figure 8-3.

Figure 8-3. The output of the ThreadStates program

You can see the messages printed by the exception handler ("Exception: Thread was being aborted") and the finally block ("ThreadFunc finishing"). Also notice that the state of the runner thread was reported as Stopped after it was aborted.

When we commented out the Sleep statement in main , rebuilt the program, and ran it again (several times), we got different results, as shown in Figure 8-4.

Figure 8-4. The ThreadStates program showing the aborted state

This time, the thread was aborted before it actually ran. Neither the exception handler nor the finally block were executed, and the state of the thread was reported as Aborted .

Depending on the speed of your computer and the number of processors, you might need to tinker with the duration of the Sleep statement in main and run the program several times to get similar results, but the important thing is to be aware that this problem can happen.

If you're calling unmanaged code inside a thread, the unmanaged code could trap and discard the ThreadAbortException . However, when the thread returns to the managed environment, the common language runtime will detect that the exception was discarded and throw it again.

Scheduling Threads

Every program that you write will use threads. When you execute a .NET application, the common language runtime will create an application domain, which will load the appropriate assemblies and then start a thread running at the entry point of your application. The static method System.Threading.Thread.get_CurrentThread() returns a reference to the currently running thread.

Thread Priorities

All threads have a priority assigned to them, which helps the operating system determine how to schedule them. Strictly speaking, thread priorities are just hints ”the operating system doesn't have to honor them (although current versions of Windows ME, Windows 2000, and Windows XP do). You can examine the priority of a thread by querying the Priority property (call the get_Priority method). This returns a value in the ThreadPriority enumeration: Highest, Above Normal, Normal, Below Normal, or Lowest . You can use the following code to determine the priority of the current thread:

 System.Threading.Thread me = System.Threading.Thread.get_CurrentThread(); ThreadPriority priority = me.get_Priority(); 

By default, the priority is ThreadPriority.Normal . You can change the priority of a thread (as long as it is not in the Aborted or Stopped state) using the set_Priority method. You specify a value from the ThreadPriority enumeration:

 me.set_Priority(ThreadPriority.Lowest); 

Assuming that the operating system does implement priorities, the scheduling mechanism should guarantee that a thread will be executed only if there are no higher-priority threads that have the Running state. Threads of the same priority will share the processor using algorithms determined by the operating system.

Note

Windows 2000 and Windows XP dynamically boost the priority of a thread that has been waiting but whose wait condition has been recently satisfied. For example, a user interface thread that has been waiting for keyboard input will have a temporarily increased priority when input is received. This ensures that threads remain responsive . After executing for a period of time, Windows will drop the thread back to its original priority.


Tip

When you're using an operating system that supports threads, changing the priority of a thread is a reliable way of ensuring that one thread will run before another. In the ThreadStates sample, rather than using Sleep to momentarily suspend the main program thread and allow runner to execute, it would be better to give the runner thread a higher priority:

 System.Threading.Thread runner = new 
 System.Threading.Thread(startProc); 
 runner.Start(); 
 runner.set_Priority(ThreadPriority.AboveNormal); Console.WriteLine("Thread running"); 
 runner.Abort(); 

You should avoid using the Highest priority for all but the most critical tasks. Even taking into account the dynamic boosting of thread priorities by Windows, high-priority threads can cause starvation among lower-priority threads, especially if they're long-lived and active (not suspended).

Background and Foreground Threads

The common language runtime further classifies threads as background or foreground . There is little difference between a background and a foreground thread. They are scheduled using the same strategies, and a high-priority background thread will override a lower-priority foreground thread. The difference between foreground and background threads becomes apparent when a multithreaded program finishes. Under normal circumstances, all threads that you create are designated as foreground, and an application keeps running as long as there is at least one foreground thread that has not completed. Background threads will be forcibly terminated (using the Abort method) when the last foreground thread in an application finishes. You can change a foreground thread to a background thread, and vice-versa, by setting the Boolean IsBackground property (use the set_IsBackground method) to true or false . (This is similar to executing setDaemon against a java.lang.Thread object.)

In the CLRThread.jsl file in the CLRSchedule project, the CLRThread class contains the usual ThreadFunc method, which is executed by a thread that is created and started by the main method of the ThreadRunner class. The ThreadFunc method contains an infinite loop that prints out a series of integers, starting at 0. The main method of ThreadRunner starts the thread running and then finishes. This will cause the thread that's running main to terminate, but the application will keep running because the thread created to run ThreadFunc is a foreground thread.

If you uncomment the statement runner.set_IsBackground(true) in the main method, build the application again, and run it, you'll find that the runner thread starts and then stops quickly when the main application thread terminates. This is because the common language runtime automatically kills all background threads when the last foreground thread for an application completes. Run the program a few times ”sometimes the background thread will not even get to start running! In these circumstances the exception handler does not run either (neither would a finally block) ”the thread is zapped without mercy.

Figure 8-5. The output of the CLRShedule program (two runs)

Threads and Unmanaged Code

It is possible for code running in the common language runtime to transition to the outside world and continue executing in an unmanaged environment. This occurs, for example, when your code calls COM objects and libraries.

COM Apartments

COM has its own threading concerns, and many volumes have been written about the joys of apartment threading (single threaded and multithreaded apartments), whose details are beyond the scope of this book. Under normal circumstances, the common language runtime does not use apartments as such. However, when a managed object calls into COM, it will create an apartment to hold the requested COM object. (Behind the scenes, the common language runtime calls the CoInitializeEx API.) You can specify whether this apartment should be single-threaded or multithreaded by setting the ApartmentState property of the thread that is calling COM to the ApartmentState.STA or ApartmentState.MTA method, respectively. You should set the ApartmentState property of a thread to match the threading model used by any COM objects that the thread will invoke; otherwise , COM will have to perform cross-apartment marshaling with all its attendant performance penalties.

Here is an example of setting the apartment state:

 System.threading.Thread runner = new System.Threading.Thread(...); runner.set_ApartmentState(ApartmentState.MTA); 

You can set this property only once for a thread ”any subsequent attempts to change the ApartmentState property will have no effect. You should also set this property early if you suspect that you'll need it; the thread does not have to have started ”it only needs to exist. If you haven't set this property, the first call to COM will result in the creation of a multithreaded apartment.

You can set the ApartmentState property of the main application thread using the System.STAThreadAttribute or System.MTAThreadAttribute at the entry point of the application (the main method). If you use Visual Studio .NET to create a Console or Windows application, you'll notice that this attribute is applied automatically to the main method:

 /** @attribute System.STAThreadAttribute() */ public static void main(String[] args) { } 

Note

Console and Windows application default to the STA apartment state primarily because threads that perform interactive IO with a user should not run in a multi-threaded apartment.


Calling Managed Code

Threads can also cross into the common language runtime from outside; you can make common language runtime objects and classes available to unmanaged code. This crossover is also achieved using COM Interop and the common language runtime exposes managed objects as if they were COM objects. Unmanaged code accesses common language runtime objects using proxy objects called COM-Callable Wrappers (CCWs). (See Chapter 13 for details.) Apartment issues are less important in this scenario because common language runtime objects act like free-threaded objects ”they can be called from single-threaded and multithreaded COM apartments. There is one major exception to this rule: Serviced Components, which are covered in detail in Chapter 14.

I l @ ve RuBoard


Microsoft Visual J# .NET (Core Reference)
Microsoft Visual J# .NET (Core Reference) (Pro-Developer)
ISBN: 0735615500
EAN: 2147483647
Year: 2002
Pages: 128

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net