One or more threads run in an AppDomain. An AppDomain is a runtime representation of a logical process within a physical process. A thread is the basic unit to which the operating system allocates processor time. Each AppDomain is started with a single thread, but can create additional threads from any of its threads.
Each thread maintains exception handlers, a scheduling priority, and a set of structures the system will use to save the thread context until it is scheduled. The thread context includes the thread's set of machine registers and stack, in the address space of the thread's process.
Operating systems that support preemptive multitasking, create the effect of simultaneous execution of multiple threads from multiple processes. On a multiprocessor computer, some operating systems that are multi-processor aware can simultaneously execute as many threads as there are processors on the computer.
A multitasking operating system divides the available processor time among the processes or threads that need it. The system is designed for preemptive multitasking; it allocates a processor time slice to each thread it executes. The currently executing thread is suspended when its time slice elapses, allowing another thread to run. When the system switches from one thread to another, it saves the thread context of the preempted thread and restores the saved thread context of the next thread in the queue.
The length of the time slice depends on the operating system and the processor. Because each time slice is small, multiple threads appear to be executing at the same time. This is actually the case on multiprocessor systems, where the executable threads are distributed among the available processors. However, caution must be used when using multiple threads in an application, because system performance can decrease if there are too many threads.
To the user, the advantage of threading is the ability to have multiple activities occurring simultaneously i.e. multiple units of execution. For example, a user can edit a spreadsheet while another thread is recalculating other parts of the spreadsheet within the same application.
To the application developer, the advantage of threading is the ability to create applications that use more than one thread of execution. For example, a process can have a user interface thread that manages interactions with the user (keyboard and mouse input), and worker threads that perform other tasks while the user interface thread waits for user input. If you give the user interface thread a higher priority, the application will be more responsive to the user, while the worker threads use the processor efficiently during the times when there is no user input.
A multithreaded process can manage mutually exclusive tasks with threads, such as providing a user interface and performing background calculations. Creating a multithreaded process can also be a convenient way to structure a program that performs several similar or identical tasks concurrently. Your AppDomain could use multiple threads to accomplish the following tasks:
It is recommended is to use as few threads as possible, thereby minimizing the use of OS resources. This improves performance. Threading has resource requirements and potential conflicts to be considered when designing your application. The resource requirements are as follows:
Providing shared access to resources can create conflicts. To avoid them, you must synchronize access to shared resources. This is true for system resources (such as communications ports), resources shared by multiple processes (such as file handles), or the resources of a single AppDomain (such as global, static and instance fields) accessed by multiple threads. Failure to synchronize access properly (in the same or in different AppDomains) can lead to problems such as deadlock and race conditions. The runtime provides synchronization objects that can be used to coordinate resource sharing among multiple threads. Reducing the number of threads makes it easier and more effective to synchronize resources.
The System.Threading.Thread class is an abstraction of the threads that execute within the Runtime. All management of threads is done through the System.Threading.Thread class, which represents a managed thread. This includes threads created by the Runtime and those created outside the Runtime that happen to be "wandering" into the Runtime environment to execute some managed code.
For reference details see: Thread.
Win32 | Runtime |
CreateThread | Combination of Thread and ThreadStart. For example if the programmer was using CreateThread(…,&Method,…) they would now use new Thread(new ThreadStart(&oFoo::Method)) . |
TerminateThread | Thread.Stop |
Thread.Abort | |
SuspendThread | Thread.Suspend |
ResumeThread | Thread.Resume |
Sleep | Thread.Sleep |
WaitForSingleObject on the thread handle | Thread.Join |
ExitThread | |
GetCurrentThread | Thread.CurrentThread |
SetThreadPriority | Thread.Priority |
No equivalent | Thread.Name |
No equivalent | Thread.IsBackground |
No equivalent | Thread.ApartmentState |
When an AppDomain is created, the Runtime creates a thread to execute the code within that AppDomain. If the code being executed is managed code, then a System.Threading.Thread object for that thread can be obtained by retrieving the static property on the thread class Thread.CurrentThread.
Creating a new instance of a System.Threading.Thread object can create new managed threads. The constructor for System.Threading.Thread takes, as its only parameter, a Thread Delegate.
A delegate is an object that acts as reference to a method on another class. A thread delegate is a special type of delegate that references a method that will be executed by a new thread.
ThreadStart
Once you have an instance of a delegate, you can pass the delegate to the constructor of a ThreadStart object. The ThreadStart object is passed to the Thread constructor. The Thread function does not begin executing until Thread.Start is called. Once Thread.Start is called, execution begins at the first line of the method referred to by the thread delegate. Calling Thread.Start more than once will cause a ThreadStateException to be thrown.
Thread.Start – Submits a start request, this is an asynchronous / request call. The method may return even while the thread has not actually started. Use Thread.ThreadState and Thread.IsAlive to determine the state of the thread.
Thread.Stop – submit a stop request, this is an asynchronous / request call. The method may return even while the thread has not actually stopped. Use Thread.ThreadState and Thread.IsAlive to determine the state of the thread.
Once a thread has been started, it’s often useful for that thread to pause for a fixed period of time. Calling Thread.Sleep causes the thread to immediately block for a fixed number of milliseconds. The Sleep method takes as a parameter a timeout, which is the number of milliseconds that the thread should remain blocked. The Sleep method is called when a thread wants to put itself to sleep. One thread cannot call Sleep on another thread. Calling Thread.Sleep(0) causes a thread to yield the remainder of its timeslice to another thread. Calling Thread.Sleep(Timeout.Infinite) causes a thread to sleep until it is interrupted by another thread that calls Thread.Interrupt.
A thread can also be paused by calling Thread.Suspend. When a thread calls Thread.Suspend on itself, the call blocks until the thread is resumed by another thread. When one thread calls Thread.Suspend on another thread, the call is a non-blocking call that causes the other thread to pause. Calling Thread.Resume breaks another thread out of the suspend state and causes the thread to resume execution. Calling Thread.Resume causes the thread to resume execution regardless of how many times Thread.Suspend was called. For example, if Thread.Suspend is called 5 consecutive times, then Thread.Resume is called, the thread will resume execution immediate following the call to Resume (it does not take 5 calls to Thread.Resume).
Unlike Thread.Sleep, Thread.Suspend does not cause a thread to immediately stop execution. The Runtime must wait until the thread has reached a safe point (defined below) before it can suspend the thread. A thread cannot be suspended if it has not been started or if it has already been stopped.
Threads can be blocked for any of several reasons. For example, a thread may be waiting for another thread to die i.e. Thread.Join, it may be waiting for access to a synchronized object i.e. Monitor.Wait, or it may just be sleeping i.e. Thread.Sleep. A thread that is waiting can be interrupted by another thread by calling Thread.Interrupt on the blocked thread. When a thread is interrupted, a ThreadInterruptedException is thrown which causes the thread to break out of the blocking call. The thread should catch the ThreadInterruptedException and do whatever is appropriate to continue working. If the thread ignores the exception, the Runtime will catch the exception and kill the thread but not before displaying a message informing the user that the thread was killed prematurely. Thread.Interrupt will also interrupt a thread that is waiting for access to a synchronized region of code.
When a Thread.Stop or Thread.Suspend is performed on a thread. The EE does not perform the action immediately. The Runtime records that a thread stop or suspend has been requested and waits until the thread has reached a safepoint before stopping or suspending the thread. A safepoint for a thread is a safe point for GC. If someone provokes a GC and the Runtime can successfully hijack a thread for the GC, the Runtime will Stop or Suspend it at that time. Hijacking is the act of bashing a return address on the stack so it returns to Runtime, rather than to its caller. This allows the Runtime to take control of threads at return points.
If a loop contains a control path with no calls somehow yield for a GC (if it had calls, hijacking would snag the thread for GC), the regular JIT detects this situation and makes the entire method fully interruptible. All instructions in the method are GC safe, because it creates GC tables for every instruction. The FJIT handles this situation by not even bothering to detect it. Instead, it makes all backward (loop-closing) branches check the EE to see if a GC is starting up.
The Thread.Stop method is used to stop a thread permanently. When stop is called, the Runtime waits for the thread to reach a safe point and then throws ThreadStoppedException. If the thread does not catch the exception, the Runtime quietly kills the thread without informing the user. The thread may choose to catch the exception and do some clean up, in which case the thread must re-throw the exception after the clean up is complete.
A ThreadStoppedException is the only exception that the Runtime will catch quietly. Any other exception caught by the Runtime will cause an Uncaught Exception message to display.
As Thread.Stop does not cause the thread to stop immediately (it waits for the thread to reach a safe point), the caller must wait on the thread if it needs to be sure the thread is indeed stopped. The caller can do this by calling Thread.Join. Join is a blocking call that does not return until the thread has actually stopped executing. Once a thread is stopped, it cannot be restarted. Calls to Thread.Suspend or Thread.Resume on a stopped thread are ignored.
The following example creates a thread, destroys it, and then calls Thread.Join to wait for it to stop executing. It tries to restart the stopped thread, however this expectantly throws the ThreadStateException
You can also call Thread.Join(int timeout) and specify a timeout period. If the thread dies before the timeout has elapsed, the call returns TRUE. Otherwise, if the time expires before the thread dies, the call returns FALSE. Threads that are waiting on a call to Thread.Join can be interrupted by other threads that call Thread.Interrupt.
Every thread has a thread priority assigned to it. Threads created within the Runtime are initially assigned the priority of Normal. Threads created outside the Runtime retain the priority they had before they entered the Runtime. You can get the priority of any thread with the Thread.Priority property and you can change the priority of any thread with Thread.Priority property.
Threads are scheduled for execution based on their priority. Even though the threads are executing within the Runtime, the threads are assigned CPU time by the OS. The details of the scheduling algorithm used to determine the order in which threads are executed varies with each OS. Under some operating systems, the thread with the highest priority (of those threads that are runable can be executed) is always scheduled to run first. If multiple threads with the same priority are all available, the scheduler does a round robin among the threads at that priority giving each thread a fixed timeslice in which to execute. As long as a thread with a higher priority is available to run, lower priority threads do not get to execute. Once there are no more runable threads at a given priority that can be executed, the scheduler moves to the next lower priority and schedules the threads at that priority for execution. Once a higher priority thread becomes runable to be executed, the lower priority thread is preempted and the higher priority thread is allowed to execute once again. On top of all that, the operating system can also adjust thread priorities dynamically as an application’s UI is moved between foreground and background. Other operating systems may choose to use a different scheduling algorithm.
A thread can be in multiple states at the same time. The property Thread.ThreadState provides a bit mask indicating the thread's current state. A thread is always in at least one of these possible states in the ThreadState enum.
Threads created within the Runtime are initially in the Unstarted state. The thread remains in the Unstarted state until it is transitioned into the started state by calling Thread.Start. External threads that wander in to the Runtime are already in the started state. Once in the started state, there are a number of actions that can cause the thread to change states. The following table lists the actions that cause a change of state along with the corresponding new state.
Action | State Transition |
---|---|
Another thread calls Thread.Start | Unchanged |
The thread starts running | Running |
The thread calls Thread.Sleep | WaitSleepJoin |
The thread calls Monitor.Wait on another object | WaitSleepJoin |
The thread calls Thread.Join on another thread | WaitSleepJoin |
Another thread calls Thread.Suspend | SuspendRequested |
The thread responds to a Thread.Suspend request | Suspended |
Another thread calls Thread.Resume | Running |
Another thread calls Thread.Stop | StopRequested |
Another thread calls Thread.Interrupt | Running |
The thread responds to a Thread.Stop request | Stopped |
Another thread calls Thread.Abort | AbortRequested |
The thread response to a Thread.Abort | Aborted |
It is often the case that a thread is in more than one state at any given time. For example, if a thread is blocked on a call to Wait and another thread calls Stop on that same thread, the thread will be in both the Waiting and the Stopping state at the same time. In that case, as soon as the thread returns from the call to Wait or is interrupted, it will receive the ThreadStopException.
Once a thread leaves the Unstarted state as the result of a call to Thread.Start, it can never return to the Unstarted state. A thread can never leave the Stopped state, either.
The following diagram illustrates the thread states and the method calls that would cause the thread to leave each state.
Figure 1: Thread State Diagram
In the Runtime, a thread is either a background thread or a foreground thread. Background threads are identical to foreground threads with one exception. Unlike a foreground thread, a background thread will not keep the Runtime alive. Once all foreground threads have been stopped, the Runtime kills all background threads and shuts down. A thread can be designated as a background or a foreground thread by setting the Thread.IsBackground property. For example a foreground thread can be designated a background thread by calling Thread.IsBackground = True. A background thread can be designated a foreground thread by calling Thread.IsBackground = False.
The Runtime kills the background threads by throwing a ThreadStoppedException. Background threads are given the opportunity to perform any cleanup by catching the exception and re-throwing it. In this situation, background threads need to handle the ThreadStoppedException in a timely fashion. If a background thread catches the exception and does not re-throw it within a reasonable period of time, the thread will be quietly killed as the Runtime is shut down.
The runtime kills the background threads by throwing a ThreadStoppedException. Background threads are given the opportunity to perform any clean up by catching the exception and re-throwing it. In this situation, background threads needs to handle the ThreadStoppedException in a timely fashion. If a background thread catches the exception and does not re-throw it within a reasonable period of time, the thread will be quietly killed as the Runtime is shut down.
Key Points
The Thread Local Store (TLS) provides data slots for the thread. The data slots are unique per thread i.e. the state is not shared across threads. Slot types:
A thread can be marked to indicate that it will host a single-threaded or multi-threaded apartment. The Thread.ApartmentState property returns and assigns the apartment state of a thread. If the property has not been set then the property returns Unknown. The property can be set when the thread is in the Unstarted or Running state, however it can be only be set once for a thread. The two valid property states are STA or MTA.
In order to perform a GC, all the threads must be suspended - except for the thread performing the GC, of course. Each thread must be brought to a “safe place” before it can be suspended. It has already been noted that threads might have to be brought to a safe place for other reasons like stopping a thread or the programmatic Thread.Suspend service. The Runtime supports bringing threads to a safe spot for the purposes of GC.
The Runtime tracks all the interesting threads in its process. Threads are interesting if they have ever executed code within the Runtime. In other words, the Runtime only tracks the threads it knows about.
Threads can enter the Runtime through COM Interop, because we expose the runtime objects out as COM objects. Other COM (not runtime) threads can then make calls on those exposed objects. Other ways a thread can enter the Runtime include DllGetClassObject() and PInvoke.
When one of our gateways, like a COM-Callable-Wrapper, is invoked on a thread, the gateway checks the TLS of that thread. It’s looking for one of our internal Thread objects. If one is found, we already know about this thread. Otherwise, it’s a thread we’ve never seen before. So we construct a new Thread object and install it in the TLS of that thread.
In addition to placing all our state about the thread in the Thread object in its TLS, we also keep a list of all the threads we’ve seen. This is the ThreadStore. It is used during thread suspension for GC and during product shutdown.