Process and Thread Revisited
Processes are used to separate the different applications that are executing at a specified time on a single computer. The operating system does not execute processes, but threads do. A thread is a unit of execution. The operating system allocates processor time to a thread for the execution of the thread's tasks. A single process can contain multiple threads of execution. Each thread maintains its own exception handlers, scheduling priorities, and a set of structures that the operating system uses to save the thread's context if the thread cannot complete its execution during the time that it was assigned to the processor. The context is held until the next time that the thread receives processor time. The context includes all the information that the thread requires to seamlessly continue its execution. This information includes the thread's set of processor registers and the call stack inside the address space of the host process.
Atomicity
In programming, an atomic action is one that effectively happens all at once. An atomic action cannot stop in the middle: it either happens completely, or it doesn't happen at all. No side effects of an atomic action are visible until the action is complete. We have already seen that an increment expression, such as i++, does not describe an atomic action. Even very simple expressions can define complex actions that can be decomposed into other actions. However, there are actions that you can specify as an atomic:
Atomic actions cannot be interleaved, so they can be used without fear of thread interference. However, this does not eliminate all need to synchronize atomic actions, because memory consistency errors are still possible. Using volatile variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What's more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change. The volatile keyword is a type qualifier used to declare that an object can be modified in the program by something such as the operating system, the hardware, or a concurrently executing thread. Specific to Microsoft, Objects declared as volatile are not used in certain optimizations because their values can change at any time. The system always reads the current value of a volatile object at the point it is requested, even if a previous instruction asked for a value from the same object. Also, the value of the object is written immediately on assignment.
Also, when optimizing, the compiler must maintain ordering among references to volatile objects as well as references to other global objects. In particular:
This allows volatile objects to be used for memory locks and releases in multithreaded applications. Using simple atomic variable access is more efficient than accessing these variables through synchronized code, but requires more care by the programmer to avoid memory consistency errors.
Windows Thread States
While a process must have one thread of execution, the process can create other threads to execute tasks in parallel. Threads share the process environment, thus multiple threads under the same process use less memory (resource) than the same number of processes. From the Win32 documentation (unmanaged), a thread can be in the following states:
The following Figure shows the Windows thread states.
While the current operating condition (execution state) of the thread can be one of the following:
From the managed (.NET) documentation, a thread is always in at least one of the possible states in the ThreadState enumeration, and can be in multiple states at the same time. The ThreadState enumeration members are:
Member name |
Description |
Running |
The thread has been started, it is not blocked, and there is no pending ThreadAbortException() |
StopRequested |
The thread is being requested to stop. This is for internal use only. |
SuspendRequested |
The thread is being requested to suspend. |
Background |
The thread is being executed as a background thread, as opposed to a foreground thread. This state is controlled by setting the Thread..::.IsBackground property. |
Unstarted |
The Thread..::.Start method has not been invoked on the thread. |
Stopped |
The thread has stopped. |
WaitSleepJoin |
The thread is blocked. This could be the result of calling Thread..::.Sleep or Thread..::.Join, of requesting a lock - for example, by calling Monitor..::.Enter or Monitor..::.Wait - or of waiting on a thread synchronization object such as ManualResetEvent. |
Suspended |
The thread has been suspended. |
AbortRequested |
The Thread..::.Abort method has been invoked on the thread, but the thread has not yet received the pending System.Threading..::.ThreadAbortException that will attempt to terminate it. |
Aborted |
The thread state includes AbortRequested and the thread is now dead, but its state has not yet changed to Stopped. |
ThreadState enumeration defines a set of all possible execution states for threads. Once a thread is created, it is in at least one of the states until it terminates. Threads created within the common language runtime (CLR) are initially in the Unstarted state, while external threads that come into the runtime are already in the Running state. An Unstarted thread is transitioned into the Running state by calling Start(). Not all combinations of ThreadState values are valid; for example, a thread cannot be in both the Aborted and Unstarted states. The following table shows the example of the actions that cause a change of state.
Action |
ThreadState |
A thread is created within the common language runtime. |
Unstarted |
A thread calls Start() |
Unstarted |
The thread starts running. |
Running |
The thread calls Sleep() |
WaitSleepJoin |
The thread calls Wait() on another object. |
WaitSleepJoin |
The thread calls Join() on another thread. |
WaitSleepJoin |
Another thread calls Interrupt() |
Running |
Another thread calls Suspend() |
SuspendRequested |
The thread responds to a Suspend() request. |
Suspended |
Another thread calls Resume() |
Running |
Another thread calls Abort() |
AbortRequested |
The thread responds to a Abort() request. |
Stopped |
A thread is terminated. |
Stopped |
In addition to the states noted above, there is also the Background state, which indicates whether the thread is running in the background or foreground. A thread can be in more than one state at a given time. For example, if a thread is blocked on a call to Wait(), and another thread calls Abort() on the blocked thread, the blocked thread will be in both the WaitSleepJoin and the AbortRequested states at the same time. In this case, as soon as the thread returns from the call to Wait() or is interrupted, it will receive the ThreadAbortException() to begin aborting. In this tutorial we will concentrate on the running state. When there are more than one threads, we need mechanisms to synchronize the threads so that all the threads will be served 'equally' by processor(s) and all the threads will have a fair access to the shared and limited resources.