A thread is a semi-process, that has its own stack, and executes a given piece of code. Unlike a real process, the thread normally shares its memory with other threads. For processes we usually have a different memory area for each one of them.
Thread Group
A Thread Group is a set of threads all executing inside the same process. They all share the same memory, and thus can access the same global variables, same heap memory, same set of file descriptors, etc. All these threads execute in parallel.
The advantage of using a thread group instead of a normal serial program is that several operations may be carried out in parallel, and thus events can be handled immediately as they arrive (for example, if we have one thread handling a user interface, and another thread handling database queries, we can execute a heavy query requested by the user, and still respond to user input while the query is being executed).
Also in a thread group context switching between threads is much faster than context switching between processes in a process group (context switching means that the system switches from running one thread or process, to running another thread or process). Also, communications between two threads is usually faster and easier to implement than communications between two processes.
Disadvantages
On the other hand, because threads in a group all use the same memory space, if one of them corrupts the contents of its memory, other threads might suffer as well. With processes, the operating system normally protects processes from one another, and thus if one corrupts its own memory space, other processes won't suffer.
Multithreading also comes with a resource and CPU cost in allocating and switching threads if used excessively. In particular, when heavy disk I/O is involved, it can be faster to have just one or two workers thread performing tasks in sequence, rather than having a multitude of threads each executing a task at the same time.
Threads in C#
A C# program starts in a single thread created automatically by the CLR and operating system (the "main" thread), and is made multi-threaded by creating additional threads. Here's a simple example and its output:
First import the following namespaces:
using System;
using System.Threading;
The main thread creates a new thread t on which it runs a method that repeatedly prints the character y. Simultaneously, the main thread repeatedly prints the character x. A thread has an IsAlive property that returns true after its Start() method has been called, up until the thread ends. A thread, once ended, cannot be re-started.
How Threading Works
Multithreading is managed internally by a thread scheduler, a function the CLR typically delegates to the operating system. A thread scheduler ensures all active threads are allocated appropriate execution time, and that threads that are waiting or blocked – for instance – on an exclusive lock, or on user input – do not consume CPU time.
On a single-processor computer, a thread scheduler performs time-slicing – rapidly switching execution between each of the active threads. This results in "choppy" behavior, such as in the very first example, where each block of a repeating X or Y character corresponds to a time-slice allocated to the thread. Under Windows XP, a time-slice is typically in the tens-of-milliseconds region – chosen such as to be much larger than the CPU overhead in actually switching context between one thread and another (which is typically in the few-microseconds region).
On a multi-processor computer, multithreading is implemented with a mixture of time-slicing and genuine concurrency – where different threads run code simultaneously on different CPUs. It's almost certain there will still be some time-slicing, because of the operating system's need to service its own threads – as well as those of other applications.
A thread is said to be preempted when its execution is interrupted due to an external factor such as time-slicing. In most situations, a thread has no control over when and where it's preempted.
Passing Data to ThreadStart
The .NET framework defines another version of the delegate called ParameterizedThreadStart, which accepts a single object argument. Consider the example below:
Here method Go is called with a parameter "true".
Naming Threads
A thread can be named via its Name property. This is of great benefit in debugging: as well as being able to Console.WriteLine a thread’s name, Microsoft Visual Studio picks up a thread’s name and displays it in the Debug Location toolbar. A thread’s name can be set at any time – but only once – attempts to subsequently change it will throw an exception.
Foreground and Background Threads
By default, threads are foreground threads, meaning they keep the application alive for as long as any one of them is running. C# also supports background threads, which don’t keep the application alive on their own – terminating immediately once all foreground threads have ended.
A thread's IsBackground property controls its background status.
Thread Priority
A thread’s Priority property determines how much execution time it gets relative to other active threads in the same process, on the following scale:
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
Locking and Thread Safety
Locking enforces exclusive access, and is used to ensure only one thread can enter particular sections of code at a time. For example, consider following class:
This is not thread-safe: if Go was called by two threads simultaneously it would be possible to get a division by zero error – because val2 could be set to zero in one thread right as the other thread was in between executing the if statement and Console.WriteLine.
Only one thread can lock the synchronizing object (in this case locker) at a time, and any contending threads are blocked until the lock is released. If more than one thread contends the lock, they are queued – on a “ready queue” and granted the lock on a first-come, first-served basis as it becomes available. Exclusive locks are sometimes said to enforce serialized access to whatever's protected by the lock, because one thread's access cannot overlap with that of another. In this case, we're protecting the logic inside the Go method, as well as the fields val1 and val2.
Thread State
Below is the thread state diagram. One can query a thread's execution status via its ThreadState property.
