Abstract
A thread is a unit of execution. Microsoft has used threads since the Win32 and NT days, and an understanding of it is necessary. In my limited knowledge, I have assembled a "manual" that could act as a reference for things like basic threading and using threading in the real world. This section, while part of the article, is only meant to be a basic introduction to the concept of threading. The one experienced in threading should overlook it. To create a thread, you must follow these steps:
- Create a method that takes no arguments and does not return any data.
- Create a new
ThreadStart
delegate and specify the method created in step 1. - Create a
Thread
object specifying the ThreadStart
object created in step 2. - Call
ThreadStart
to begin execution of the new thread. The code will look something like this:
using System;
using System.Threading;
public class Program {
public static void Main() {
ThreadStart operation = new ThreadStart(SimpleWork);
Thread thr = new Thread(operation);
thr.Start();
}
private static void SimpleWork()
{
Console.WriteLine("Thread: {0}",
Thread.CurrentThread.ManagedThreadId);
}
}
Here are some of the Thread
class' properties:
IsAlive
: Gets a value indicating that the current thread is currently executing.IsBackground
: Gets or sets whether the thread runs as a background thread.IsThreadPoolId
: Gets whether this thread is a thread in the thread pool.ManagedThreadId
: Gets a number to identify the current thread.Name
: Gets or sets a name associated with the thread.Priority
: Gets or sets the priority of the thread.ThreadState
: Gets the ThreadState
value for the thread.
A more likely scenario than the example shown above is one in which you will want to create multiple threads:
using System;
using System.Threading;
public class Program {
public static void Main() {
ThreadStart operation = new ThreadStart(SimpleWork);
for (int x = 1; x <= 5; ++x)
{
Thread thr = new Thread(operation);
thr.Start();
}
}
private static void SimpleWork()
{
Console.WriteLine("Thread: {0}", Thread.CurrentThread.ManagedThreadId);
}
}
Output:
Thread: 3
Thread: 4
Thread: 5
Thread: 6
Thread: 7
Here are some of Thread
's methods (not static!!):
Abort
: Raises a ThreadAbort
exception on the thread to indicate that the thread should be aborted.Interrupt
: Raises a ThreadInterruptException
when a thread is in blocked state.Join
: Blocks the calling thread until the thread terminates.Start
: Sets a thread to be scheduled for execution.
Using Thread.Join
is sometimes necessary because more often than not, you will need your application to wait for a thread to complete execution. To accomplish this, the Thread
class supports the Join
method, which is a static method:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Threading;
class InterruptAwareWorker
{
private bool interruptRequested;
private Thread myThread;
public void Interrupt()
{
if (myThread == null)
throw new InvalidOperationException();
interruptRequested = true;
myThread.Interrupt();
myThread.Join();
}
private void CheckInterrupt()
{
if (interruptRequested)
throw new ThreadInterruptedException();
}
public void DoWork(object obj)
{
myThread = Thread.CurrentThread;
try
{
while (true)
{
CheckInterrupt();
CheckInterrupt();
}
}
catch (ThreadInterruptedException)
{
Console.WriteLine("Thread was interrupted...");
return;
}
}
public static void Main()
{
InterruptAwareWorker w = new InterruptAwareWorker();
Thread t = new Thread(w.DoWork);
t.Start();
w.Interrupt();
t.Join();
}
}
Output:
Thread was interrupted...
This code runs as a console application in Visual Studio 2010,and therefore will only run on .NET 4.0, yet on the command line. For the sake of understanding the concept, here is an example of joining threads:
using System;
using System.Threading;
public class Program {
public static void Main()
{
int threadCount = 5;
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++)
{
int idx = i;
threads[i] = new Thread(delegate() { Console.WriteLine("Worker {0}", idx); });
}
Console.WriteLine("Beginning thread execution...");
Array.ForEach(threads, delegate(Thread t) { t.Start(); });
Console.WriteLine("Waiting for completion...");
Array.ForEach(threads, delegate(Thread t) { t.Join(); });
Console.WriteLine("All threads complete");
}
}
The result is as expected. Note that when we deal with multiple threads, we need to wait on all our threads. We can do this by keeping reference to all of our threads and calling Join
on each of the threads to wait for the threads to complete:
Beginning thread execution...
Worker 0
Worker 1
Worker 2
Worker 3
Waiting for completion...
Worker 4
All threads complete
Now in the earlier examples, we were using the ThreadStart
delegate, which takes no parameters. In practice, you will need to pass information to individual threads. To do this, you need to use a new delegate called ParamterizedThreadStart
. This delegate specifies a method signature with a single parameter of type Object
and returns nothing. Here is an example. Notice that we are passing data to a thread by using this delegate:
using System;
using System.Threading;
public static class Program {
public static void Main() {
ParameterizedThreadStart operation =
new ParameterizedThreadStart(WorkWithParameter);
Thread theThread = new Thread(operation);
theThread.Start("hello");
Thread newThread = new Thread(operation);
newThread.Start("goodbye");
}
private static void WorkWithParameter(object o)
{
string info = (string) o;
for (int x = 0; x < 10; ++x)
{
Console.WriteLine("{0}: {1}", info,
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(10);
}
}
}
Output:
hello: 3
goodbye: 4
hello: 3
goodbye: 4
hello: 3
goodbye: 4
hello: 3
goodbye: 4
hello: 3
goodbye: 4
goodbye: 4
hello: 3
goodbye: 4
hello: 3
goodbye: 4
hello: 3
hello: 3
goodbye: 4
goodbye: 4
hello: 3
Examine the created method WorkWithParameter(object o)
. This is a method that takes a single Object
parameter (and therefore can be a reference to any object). To use this as the starting point of a thread call, you can create a ParameterizedThreadStart
delegate to point at this new method and use the Thread.Start
method's overload that takes a single object
parameter.
Where this Leads to
The topic of threading can sometimes fog the beginner once he or she enters the topics of synchronization, concurrency, parallelism, and the like. Let's takes the lock
statement. The C# lock
statement is really just a shorthand notation for working with the System.Threading.Monitor
class type. Thus, if you were to look under the hood to see what lock()
actually resolves, you would find code like the following:
using System;
using System.Threading;
public class WhatIsAThread
{
private long refCount = 0;
public void AddRef()
{
Interlocked.Increment(ref refCount);
}
public void Release()
{
if(Interlocked.Decrement(ref refCount) == 0)
{
GC.Collect();
}
}
}
internal class WorkerClass
{
public void DoSomeWork()
{
lock(this)
{
for(int i = 0; i < 5; i++)
{
Console.WriteLine("Worker says: " + i + ", ");
}
}
Monitor.Enter(this);
try
{
For (int i = 0; i < 5; i++)
{
Console.WriteLine("Worker says: " + i + ", ");
}
}
finally
{
Monitor.Exit(this);
}
}
}
public class MainClass
{
public static int Main(string[] args)
{
WorkerClass w = new WorkerClass();
Thread workerThreadA = new Thread(new ThreadStart(w.DoSomeWork));
Thread workerThreadB = new Thread(new ThreadStart(w.DoSomeWork));
Thread workerThreadC = new Thread(new ThreadStart(w.DoSomeWork));
workerThreadA.Start();
workerThreadB.Start();
workerThreadC.Start();
return 0;
}
}
Output:
Worker says: 0,
Worker says: 1,
Worker says: 2,
Worker says: 3,
Worker says: 4,
Worker says: 0,
Worker says: 1,
Worker says: 2,
Worker says: 3,
Worker says: 4,
Worker says: 0,
Worker says: 1,
Worker says: 2,
Worker says: 3,
Worker says: 4,
Threads: A Deeper Look
This section of the article will continue to be a reference for threading, but will also include the Operating System environment in which a thread can execute. Of the kernel objects, we will then cover the event thread synchronization object. Examining the environment can help us better understand how to effectively achieve concurrency and multithreading. The topic of thread creation will come later on in this article. We also want to know why threads can cost: stated loosely, threads are expensive. Each thread is provided with:
- Thread kernel object: The OS allocates and initializes one of these data structures for each thread created in the system. The data structure contains a bunch of properties (discussed later in this chapter) that describe the thread. This data structure also contains what is called the thread's context. The context is a block of memory that contains a set of the CPU's registers. When Windows is running on a machine with an x86 CPU, the thread's context uses about 700 bytes of memory. For x64 and IA64 CPUs, the context is about 1,240 and 2,500 bytes of memory, respectively.
- Thread environment block (TEB): The TEB is a block of memory allocated and initialized in user mode (address space that application code can quickly access). The TEB consumes 1 page of memory (4 KB on x86 and x64 CPUs, 8 KB on an IA64 CPU). The TEB contains the head of the thread's exception-handling chain. Each
try
block that the thread enters inserts a node in the head of this chain; the node is removed from the chain when the thread exists the try
block. In addition, the TEB contains the thread's thread-local storage data as well as some data structures for use by the Graphics Device Interface (GDI) and OpenGL graphics. - User-mode stack: The user-mode stack is used for local variables and arguments passed to methods. It also contains the address indicating what the thread should execute next when the current method returns. By default, Windows allocates 1 MB of memory for each thread's user-mode stack.
- Kernel-mode stack: The kernel-mode stack is also used when the application code passes arguments to a kernel-mode function in the Operating System. For security reasons, Windows copies any arguments passed from user-mode code to the kernel from the thread's user-mode stack to the thread's kernel-mode stack. Once copied, the kernel can verify the argument values, and since the application code can't access the kernel mode stack, the application can't modify the argument values after they have been validated and the OS kernel code begins to operate on them. In addition, the kernel calls methods within itself and uses the kernel-mode stack to pass its own arguments, to store a function's local variables, and to store return addresses. The kernel-mode stack is 12 KB when running on a 32-bit Windows system, and 24 KB when running on a 64-bit Windows system.
Using multiple threads for a single program can be done to run entirely independent parts of the program at once. This is called concurrency, and is frequently used in server-side applications. Using threads to break one big task down into multiple pieces that can execute concurrently is called parallelism. Conceptually speaking, a thread is unit of execution - an execution context that represents in-progress work being performed by a program. Windows must allocate a kernel object for each thread, along with a set of data structures. Each thread is mapped onto a processor by the Windows thread scheduler, enabling the in-progress work to actually execute. Each thread has an Instruction Pointer that refers to the current executing instruction. "Execution" consists of the processor fetching the next instruction, decoding it, and issuing it, one instruction after the other, from the thread's code. During the execution of some compiled code, program data will be routinely moved into and out of registers from the attached main memory. While these registers physically reside on the processor, some of the volatile state also belongs to the thread too. If the thread must be paused, this state will be captured and saved in memory so it can be later restored. Doing this enables the same IP fetch, decode, and issue process to proceed for the thread as though it was never interrupted. The process of saving or restoring this state from and to the hardware is called a context switch.
Execution Context
As in Windows, each thread in .NET has data associated with it, and that data is usually propagated to new threads. This data includes security information (the IPrinciple
and thread identity), the localization strings, and transaction information from System.Transaction
. By default, the execution context flows to helper threads, but this is costly: a context switch is a heavy-weight operation. To access the current execution context, the ExecutionContext
class supplies static methods to control the flow of context information. So in the System.Threading
namespace, there is an ExecutionContext
class that allows you to control how a thread's execution context flows from one thread to another. Here is what the class looks like:
public sealed class ExecutionContext : IDisposable, ISerializable {
[SecurityCritical]public static AsyncFlowControl SuppressFlow();
public static void RestoreFlow();
public static Boolean IsFlowSuppressed();
}
You can use this class to suppress the flowing of an execution context, thereby improving your application's performance. The performance gains can be quite substantial for a server application. There is not much performance benefit for a client application, and the SuppressFlow
method is marked with the [SecurityCritical]
attribute, making it impossible to call in some client applications (like Silverlight). Of course, you should suppress the flowing of execution context only if the helper thread does not need or access the context information. If the initiating thread's execution context does not flow to a helper thread, the helper thread will use whatever execution context is last associated with it. Therefore, the helper thread really shouldn't execute any code that relies on the execution context state (such as a user's Windows identity). The example shown next is MSDN code, and compiles with warnings on .NET 4.0. It compiles and executes normally on .NET 2.0.
using System;
using System.Threading;
using System.Security;
using System.Collections;
using System.Security.Permissions;
using System.Runtime.Serialization;
using System.Runtime.Remoting.Messaging;
namespace Contoso
{
class ExecutionContextSample
{
static void Main()
{
try
{
Console.WriteLine("Executing Main in the primary thread.");
FileDialogPermission fdp = new FileDialogPermission(
FileDialogPermissionAccess.OpenSave);
fdp.Deny();
ExecutionContext eC = ExecutionContext.Capture();
AsyncFlowControl aFC = ExecutionContext.SuppressFlow();
Thread t1 = new Thread(new ThreadStart(DemandPermission));
t1.Start();
t1.Join();
Console.WriteLine("Is the flow suppressed? " +
ExecutionContext.IsFlowSuppressed());
Console.WriteLine("Restore the flow.");
aFC.Undo();
Console.WriteLine("Is the flow suppressed? " +
ExecutionContext.IsFlowSuppressed());
Thread t2 = new Thread(new ThreadStart(DemandPermission));
t2.Start();
t2.Join();
CodeAccessPermission.RevertDeny();
ExecutionContext eC2 = ExecutionContext.Capture();
Thread t3 = new Thread(new ThreadStart(DemandPermission));
t3.Start();
t3.Join();
Thread t4 = new Thread(new ThreadStart(DemandPermission));
t4.Start();
t4.Join();
ExecutionContextMethods();
Console.WriteLine("Demo is complete, press Enter to exit.");
Console.Read();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
static void DemandPermission()
{
try
{
Console.WriteLine("In the thread executing a Demand for " +
"FileDialogPermission.");
new FileDialogPermission(
FileDialogPermissionAccess.OpenSave).Demand();
Console.WriteLine("Successfully demanded " +
"FileDialogPermission.");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
static void ExecutionContextMethods()
{
ContextBoundType cBT = new ContextBoundType();
cBT.GetServerTime();
ExecutionContext eC1 = ExecutionContext.Capture();
ExecutionContext eC2 = eC1.CreateCopy();
Console.WriteLine("The hash code for the first execution " +
"context is: " + eC1.GetHashCode());
SerializationInfo sI = new SerializationInfo(
typeof(ExecutionContext),
new FormatterConverter());
eC1.GetObjectData(
sI,
new StreamingContext(StreamingContextStates.All));
LogicalCallContext lCC = (LogicalCallContext)sI.GetValue(
"LogicalCallContext",
typeof(LogicalCallContext));
Console.WriteLine("Is the logical call context information " +
"available? " + lCC.HasInfo);
}
}
[Serializable]
public class CallContextString : ILogicalThreadAffinative
{
String _str = "";
public CallContextString(String str)
{
_str = str;
Console.WriteLine("A CallContextString has been created.");
}
public override String ToString()
{
return _str;
}
}
public class ContextBoundType : ContextBoundObject
{
private DateTime starttime;
public ContextBoundType()
{
Console.WriteLine("An instance of ContextBoundType has been " +
"created.");
starttime = DateTime.Now;
}
[SecurityPermissionAttribute(SecurityAction.Demand,
Flags = SecurityPermissionFlag.Infrastructure)]
public DateTime GetServerTime()
{
Console.WriteLine("The time requested by a client.");
CallContext.SetData(
"ServerThreadData",
new CallContextString("This is the server side replacement " +
"string."));
return DateTime.Now;
}
}
}
Output:
Executing Main in the primary thread.
In the thread executing a Demand for FileDialogPermission.
Successfully demanded FileDialogPermission.
Is the flow suppressed? True
Restore the flow.
Is the flow suppressed? False
In the thread executing a Demand for FileDialogPermission.
Request for the permission of type 'System.Security.Permissions.FileDialogPermis
sion, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e08
9' failed.
In the thread executing a Demand for FileDialogPermission.
Successfully demanded FileDialogPermission.
In the thread executing a Demand for FileDialogPermission.
Successfully demanded FileDialogPermission.
An instance of ContextBoundType has been created.
The time requested by a client.
A CallContextString has been created.
The hash code for the first execution context is: 54267293
Is the logical call context information available? True
Demo is complete, press Enter to exit.
Threads run Within a Process but Processes Don't Run: Threads Run.
Processes in the .NET Framework correspond one to one with a process in Windows. A process' main purpose is to manage per-program resources; this includes a shared virtual address space among all threads running in the process, a HANDLE table, a shared list of loaded DLLs (mapped into the same address space), and a variety of other process-wide data stored in the Process Environment Block (PEB). Problems in one process normally do not affect another because of this type of isolation. However, because of inter-process communication and machine-wide shared resources -- such as files, memory-mapped I/O, and named kernel objects--sometimes one process can interfere with another process.
Windows provides four objects designed for thread and process synchronization--mutexes, semaphores, events (all of which are kernel objects), and a critical_section object. Events are a kernel synchronization object used to signal other threads that some event, such as a message, has occurred. The important capability offered by events is that multiple threads can be released from a wait simultaneously when a single event is signaled. In .NET, there is an Event
class where an event (a type of kernel object) can be one of two types: auto reset and manual reset. When an auto reset event is signaled, the first object waiting for the event turns its back to a non-signaled state. This behavior is similar to that of a mutex. Conversely, a manual reset event allows all threads waiting for it to become unblocked until something manually resets the event to a non-signaled state. These events are represented as the AutoResetEvent
class and ManualResetEvent
class in .NET. Both of these classes inherit from a common EventWaitHandle
class (which itself handles the WaitHandle
class). Assume we want to write some code to schedule some work on the thread-pool:
using System;
using System.Threading;
public sealed class Program {
private static void MyThreadPoolWorker(object state)
{
ManualResetEvent mre = (ManualResetEvent)state;
Console.WriteLine("Work occurring on the thread-pool: {0}",
Thread.CurrentThread.ManagedThreadId);
mre.Set();
}
public static void Main()
{
using (ManualResetEvent mre = new ManualResetEvent(false))
{
ThreadPool.QueueUserWorkItem(new WaitCallback(MyThreadPoolWorker), mre);
Console.WriteLine("Continuing work on the main thread: {0}",
Thread.CurrentThread.ManagedThreadId);
mre.WaitOne();
}
}
}
This example shows registering wait callbacks for events:
using System;
using System.Threading;
public sealed class Program {
public static void Main()
{
using (EventWaitHandle ewh = new ManualResetEvent(false))
using (EventWaitHandle callbackDoneEvent = new ManualResetEvent(false))
{
ThreadPool.RegisterWaitForSingleObject(ewh,
delegate {
Console.WriteLine("Callback fired: {0}",
Thread.CurrentThread.ManagedThreadId);
callbackDoneEvent.Set();
}, null, Timeout.Infinite, true);
Console.WriteLine("Setting the event: {0}",
Thread.CurrentThread.ManagedThreadId);
ewh.Set();
callbackDoneEvent.WaitOne();
}
}
}
The result:
Setting the event: 1
Callback fired: 4
Continuing work on the main thread: 1
Work occurring on the thread-pool: 3
Thread Creation
To create a new thread in the .NET Framework, a Thread
object must first be created by of Threadd
's many constructors:
public delegate void ThreadStart();
public delegate void ParameterizedThreadStart(object obj);
public class Thread
{
public Thread(ThreadStart start);
public Thread(ThreadStart start, int maxStackSize);
public Thread(ParameterizedThreadStart start);
public Thread(ParameterizedThreadStart start, int maxStackSize);
. . .
}
Recall that a thread created with the ParameterizedThreadStart
based constructor allows a caller to pass an object reference argument to the Start
method (as a parameter), which is then accessible from the new thread's Start
routine as obj
:
using System;
using System.Threading;
public static class Program
{
public static void Main()
{
Thread newThread = new Thread(
new ParameterizedThreadStart(MyThreadStart));
Console.WriteLine("{0}: Created thread (ID {1})",
Thread.CurrentThread.ManagedThreadId,
newThread.ManagedThreadId);
newThread.Start("Hello world");
newThread.Join();
Console.WriteLine("{0}: Thread exited",
Thread.CurrentThread.ManagedThreadId);
}
private static void MyThreadStart(object obj)
{
Console.WriteLine("{0}: Running: {1}",
Thread.CurrentThread.ManagedThreadId, obj);
}
}
The results:
1: Created thread (ID 3)
3: Running: Hello world
1: Thread exited
Here is a thread creation example that uses anonymous delegates:
using System;
using System.Threading;
public static class Program
{
public static void Main()
{
Thread newThread = new Thread(delegate(object obj)
{
Console.WriteLine("{0}: Running {1}",
Thread.CurrentThread.ManagedThreadId, obj);
});
newThread.Start("Hello world (with anon delegates)");
newThread.Join();
}
}
Output:
3: Running Hello world (with anon delegates)
Finally, here is an example of thread creation using lambdas:
using System;
using System.Threading;
public static class Program
{
public static void Main()
{
Thread newThread = new Thread(obj =>
Console.WriteLine("{0}: Running {1}",
Thread.CurrentThread.ManagedThreadId, obj)
);
newThread.Start("Hello world (with lambdas)");
newThread.Join();
}
}
Output:
3: Running Hello world (with lambdas)
So what have we got? We see that a thread exits. Is the state of the system the same if the thread is abruptly terminated? We know that an application program will frequently block execution so that it may perform I/O; for example, reading the sectors on the disk, communicating with a network endpoint, etc. But UIs work by processing messages enqueued onto a per-UI-thread message queue. Several types of blocking enable the UI's message pump to run. But others do not. This can cause messages (e.g., WM_CLOSE
, WM_PAINT
, etc.) to get clogged in the queue until the I/O completes (i.e., it runs synchronously). For lengthy operations, this can lead to an unresponsive UI. Here is an example of a small Windows Forms program that demonstrates the very basics of maintaining the UI:
using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
class Program : Form
{
private System.Windows.Forms.ProgressBar _ProgressBar;
[STAThread]
static void Main()
{
Application.Run(new Program());
}
public Program()
{
InitializeComponent();
ThreadStart threadStart = Increment;
threadStart.BeginInvoke(null, null);
}
void UpdateProgressBar()
{
if (_ProgressBar.InvokeRequired)
{
MethodInvoker updateProgressBar = UpdateProgressBar;
_ProgressBar.Invoke(updateProgressBar);
}
else
{
_ProgressBar.Increment(1);
}
}
private void Increment()
{
for (int i = 0; i < 100; i++)
{
UpdateProgressBar();
Thread.Sleep(100);
}
}
private void InitializeComponent()
{
_ProgressBar = new ProgressBar();
SuspendLayout();
_ProgressBar.Location = new Point(13, 17);
_ProgressBar.Size = new Size(267, 19);
ClientSize = new Size(292, 53);
Controls.Add(this._ProgressBar);
Text = "Multithreading in Windows Forms";
ResumeLayout(false);
}
}
The CLR Thread Pool basics involve queuing up a chunk of work that will be run by the thread pool, use the pool to run some work when asynchronous I/Os complete, execute work on a recurring or timed basis using timers, and/or schedule some work to run when a kernel object becomes signaled. There are articles about concurrency at MSDN that describe either managing the thread pool order or outright building a custom thread pool to optimize the execution context.
References
- The CLR via C#, 3rd Edition, by Jeffrey Richter
- Professional .NET Framework 2.0, by Joe Duffy
- Microsoft .NET Framework 2.0 Application Development Foundation