Threading in the .NET framework
Lesson 1: Creating Threads
Thread is basis of high-performance applications.
Simple Threads
The Thread class represents a single thread. It supports properties that indicate if the thread is currently executing, if it is part of a thread pool, what its unique identifier is, its priority, etc. Each thread sports methods that allow it to be aborted, started, block a calling thread until the Thread completes, etc.
The class also has static properties that identify the ThreadContext associated with the currently running Thread, the user account under which threads are executing and the Thread currently running.
Static Thread methods include functions to indicate sections of code that are critical and cannot be safely aborted, to get the AppDomain associated with the running thread, to send the currently running thread to sleep for a specified duration, to block the current thread form executing (but not relinquish control to others), etc.
Creating threads
Create void method that takes no arguments
static void SimpleWork() {...};
Create ThreadStart delegate specifying the method from 1.
ThreadStart operation = new ThreadStart(SimpleWork);
Create a Thread object, specifying the ThreadStart object from 2.
Thread theThread = new Thread(operation);
Call Thread .Start to begin execution of the thread.
theThread.Start();
Multiple Threads
To create multiple threads simply create multiple Thread objects, e.g.
for (int x = 1; x <= 5; ++x)
{
Thread theThread = new Thread(operation);
theThread.Start();
}
Using Thread.Join
Frequently an application will have to wait for a thread to complete execution before continuing with another task. The Thread.Join method tells system to make application wait until thread has completed. For example to stop the application terminating before all thread have completed, record each thread started in an array and then wait for each thread to halt, e.g.
Thread[] theThreads = new Thread[5];
for (int x = 1; x <= 5; ++x)
{
theThreads[x] = new Thread(operation);
theThreads[x].Start();
}
foreach(Thread t in theThreads)
{
t.Join();
}
Thread Priority
Get or set using the Priority property which takes a ThreadPriority enumeration (Highest, AboveNormal, Normal, BelowNormal, Lowest).
Scheduling takes place based on this enumeration. Choosing priority below Normal can cause scheduler to starve thread of processing time more than expected, choosing higher priorities can starve the system. Use caution when choosing non-Normal priorities.
Passing Data To Threads
Instead of using ThreadStart delegate, use the ParameterizedThreadStart delegate which takes a single parameter of Object and returns void, e.g.
ParameterizedThreadStart operation = new ParameterizedThreadStart(WorkWithParameter);
Thread theThread = new Thread(operation);
theThread.Start("Hello");
As the ParameterizedThreadStart delegate takes an object anything could be passed to the thread function and appropriate checks should be made before use.
Stopping Threads
Primary mechanism is the ThreadAbort method. This causes the operating system to throw a ThreadAbortException within the thread. Whether or not the exception is caught, the thread is stopped after the exception is thrown.
If the ThreadAbortException is not caught then execution of the thread will stop immediately that ThreadAbort is called. This can be a problem as the system does not know if it can safely kill the thread (aborting in the wrong place could cause data to be inconsistent).
To solve this problem the Thread class supports the static methods BeginCriticalRegion and EndCriticalRegion. Place these calls around code that must be executed atomically, e.g.
static void MainThread()
{
Thread.BeginCriticalRegion();
SomeClass.IsValid = true;
SomeClass.IsComplete = true;
Thread.EndCriticalRegion();
}
In .NET 2.0 the Thread.Suspend and Thread.Resume methods have been deprecated. Instead use synchronisation methods discussed later.
Execution Context
Each thread has data associated with it, this data is usually propagated to new threads. The data includes security information (the IPrincipal and thread identity), localisation settings and transaction information from System.Transactions. To access this information use the ExecutionContext class.
By default the execution context flows to helper threads - but at a cost. To increase performance, but loose current security, culture and transaction information call ExecutionContext.SuppressFlow e.g.
AsyncFlowControl flow = ExecutionContext.SupressFlow();
Thread thread = new Thread(new ThreadStart(SomeWork));
thread.Start();
thread.Join();
flow.RestoreFlow();
The Run method of the ExecutionContext allows execution of arbitrary code on the current thread with custom context information. The current context can be retrieved using the ExecutionContext.Capture method. The arbitrary code to call is referenced via the ContextCallback delegate.
ExecutionContext ctx = ExecutionContext.Capture();
ExecutionContext.Run(ctx, new ContextCallback(SomeMethod), null);
Lesson 2: Sharing Data
Avoiding Collisions
Multiple threads can attempt to access the same data simultaneously.
When dealing with access to single variables can use Interlocked class which sports the following static methods:
- Add - add two integers as an atomic operation
- Decrement - subtracts one from a value as an atomic operation
- Exchange - swaps two values as an atomic operation
- Increment - adds one to a value as an atomic operation
- Read - read 64 bit number as atomic operation. Required for 32 bit systems as 64 bit numbers are represented as two pieces of information.
static void UpdateCount()
{
for (int x = 1; x < 10000; x++)
{
Interlocked.Increment(ref Counter.Count);
}
}
The Thread class supports Volatile data. Volatile reads and writes prevent caching of data within CPU to prevent inconsistencies in threaded applications. Instead of using Threads VolatileRead and VolatileWrite methods (or the C# volatile keyword) use the Interlocked classes methods as they are portable across various CPU architectures.
Main problem with Interlocked class is that it supports a small subset of the .NET data types. For other data types or larger blocks of codes use synchronisation locks.
Synchronisation Locks
The following code...
public void UpdateCount()
{
Interlocked.Increment(ref _count);
if (_count % 2 == 0)
{
Interlocked.Increment(_evenCount);
}
}
does not always work as expected when called in a multi-threaded environment. The _count and _evenCount values can become out of step as the two operations do not occur as an atomic operation. To solve this problem use the lock keyword...
public void UpdateCount()
{
lock(this)
{
_count++;
if (_count % 2 == 0)
{
_evenCount++;
}
}
}
The lock requires an object to be used as its identifier. For code that manipulates several items of data within a class can use the current class instance. The synchronisation lock blocks other threads from accessing the code while a thread is within the lock.
C# keywords are easy ways to create locks, but there may need to be more control over the way a lock is created. Internally the lock keyword uses the Monitor class to perform synchronisation.
Important static Monitor methods:
- Enter - create exclusive lock on specified object
- Exit - release exclusive lock on specified object
- TryEnter - attempt to create exclusive lock on specified object (optionally supports time-out value on acquiring lock)
- Wait - releases exclusive lock and blocks current thread until it can re-acquire the lock
public void UpdateCount()
{
Monitor.Enter(this);
try
{
_count++;
if (_count % 2 == 0)
{
_evenCount++;
}
}
finally
{
Monitor.Exit(this);
}
}
Synchronisation locks can cause problems known as deadlocks.
Deadlocks
Two pieces of code try and access data held by the other.
There is no magic class to solve deadlocks. Instead must use careful development and detection.
- Use Monitor.TryEnter with time outs to allow deadlocks to recover
- Reduce the amount of code that is locked to reduce time that a resource is locked.
Other Synchronisation Methods
Monitor class is useful (and lightweight) tool for developing threaded software.
Other synchronisation mechanisms have their own uses.
ReadWriterLock class
Differentiates between two classes of code that can use certain resources.
Via this class can lock access to readers and writers separately.
Allows multiple readers to access code simultaneously, but only a single writer can get a lock on the data.
All readers must release locks before a writer can gain access.
When reading data...
ReaderWriterLock rwLock = new ReaderWriterLock();
int counter = 0;
try
{
rwLock.AcquireReaderLock(100);
try
{
Console.WriteLine(counter);
}
finally
{
rwLock.ReleaseReaderLock();
}
}
catch (ApplicationException)
{
Console.WriteLine("Failed to get reader lock");
}
When changing data...
ReaderWriterLock rwLock = new ReaderWriterLock();
int counter = 0;
try
{
rwLock.AcquireWriterLock(100);
try
{
Interlocked.Increment(counter);
}
finally
{
rwLock.ReleaseWriterLock();
}
}
catch (ApplicationException)
{
Console.WriteLine("Failed to get writer lock");
}
It is possible to upgrade a reader lock to a writer lock (using UpgradeToWriterLock) and downgrade to reader lock (using DowngradeFromWriterLock). These methods need to be used in tandem (like Monitor.Enter and Monitor.Exit). The UpgradeToWriterLock returns a LockCookie structure that is later used to downgrade the lock.
try
{
LockCookie cookie = rwLock.UpgradeToWriterLock(1000);
counter++;
rwLock.DowngradeFromWriterLock(ref cookie);
}
The UpgradeToWriterLock call requires a time out value and might fail to acquire the writer lock (like any acquisition of a writer lock). The DowngradeFromWriterLock requires no time out as it is releasing the writer lock and reinstituting the reader lock.
Synchronisation with Kernel Objects
The OS supports three kernel synchronisation objects - Mutex, Semaphore and Event. Powerful, but heavy, e.g. Mutex is 33 times slower than Monitor, but they permit synchronisation impossible with Monitor.
- Mutex allows synchronisation across AppDomain and process boundaries
- Semaphore throttles access to a resource to a set number of threads
- Event notifies multiple threads that an event has occurred
Each of these objects is represented by .NET classes (Mutex, Semaphore, AutoResetEvent and ManualResetEvent).
Mutex Class
Provides lock mechanism, works in similar way to Monitor but can lock across AppDomain and process boundaries. Usually create with well known name so it can be accessed across AppDomain or process boundaries (by using the static OpenExisting method), e.g.
try
{
theMutex = Mutex.OpenExisting("MYMUTEX");
}
catch (WaitHandleCannotBeOpenedException)
{
theMutex = new Mutex(false, "MYMUTEX");
}
Semaphore Class
New to .NET 2.0
When creating semaphore specify current number of used slots and maximum slots. When releasing specify how many slots to release.
Semaphore theSemaphore = new Semaphore(0, 10);
...
theSemaphore.Release(5);
Event class
Kernel object with two states - on and off. Allow threads across application to wait until event is signalled to do something. Two types - auto reset and manual. When auto-reset is signalled the first object waiting for the event turns it back to a non-signalled state (similar behaviour to Mutex). Manual reset event allows all threads that are waiting for it to become unblocked until something manually resets the event to a non-signalled state.
Lesson 3: Asynchronous Programming Model
Many classes support APM by supplying BeginXXX and EndXXX versions of methods, e.g. FileStream has Read method to extract data from stream. To support APM it exposes BeginRead and EndRead methods.
The BeginRead method returns an IAsyncResult instead of bytes read.
The EndRead method ends the asynchronous operation. Call it with the IAsyncResult object from BeginRead to obtain the bytes actually read.
How do you know when to call EndRead?
Wait-Until-Done Model
- Start the asynchronous call
- Perform some other work
- Attempt to end the asynchronous operation and wait for the asynchronous call to complete
When calling BeginRead null values are specified for the callback and state objects as they are not used.
IASyncResult result = strm.BeginRead(buffer, 0, buffer.Length, null, null);
// Do some work...
int numBytes = strm.EndRead(result);
Polling Model
Similar to before with the exception that the IASyncResult is polled to see if it has completed.
IASyncResult result = strm.BeginRead(buffer, 0, buffer.Length, null, null);
while (!result.IsComplete)
{
// Do some work...
}
int numBytes = strm.EndRead(result);
Callback Model
Must specify method to callback on together with any state information required by the callback to complete the call. The following will create a new AsyncCallback delegate specifying a method to call (on another thread) when the operation is complete. The stream object "strm" is being passed as the state information for the call.
IAsyncResult result = Strm.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(CompleteRead), strm);
The callback method looks something like...
static void CompleteRead(IAsyncResult result)
{
FileStream strm = (FileStream) result.AsyncState;
int numBytes = strm.EndRead(result);
strm.Close();
}
Exceptions and APM
When using APM there may be operations that throw exceptions during the asynchronous processing of a request. To allow for this exceptions are thrown during the EndXXX call - it is not thrown the moment the exception happens.
To handle exceptions do so during the processing of the EndXXX call, e.g. To report any IOExceptions do the following...
int numBytes = 0;
try
{
numBytes = strm.EndRead(result);
}
catch (IOException)
{
Console.WriteLine("An IO exception occurred");
}
Windows Forms Exception Handling
Unhanded exceptions (either in main thread or during asynchronous calls) result in standard dialogue box being displayed and the application being terminated. Can change this behaviour by registering for ThreadException event on the Application class, e.g.
Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
...
static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
{...}
Using the ThreadPool
In many cases creating your own threads is not necessary. The .NET system supports built in thread pool that can be used in many situations where you would expect to create your own threads, e.g.
static void WorkWithParameter(object o)
{...}
WaitCallback workItem = new WaitCallback(WorkWithParameter);
ThreadPool.QueueUserWorkItem(workItem, "ThreadPooled");
This is not a short cut for creating a thread. Instead it provides access to set of threads maintained by .NET and made available to applications. Thread pool is fast because the threads are reused as necessary saving set up costs. Additionally it throttles the number of threads in use by a process by queuing up work to be performed. As threads in the pool become available, the pool posts new work to the thread.
ThreadPool class provides static methods to queue work items and control the operations of the pool.
- GetAvailableThreads - number of threads available in pool
- GetMaxThreads - maximum number of threads the thread pool can support
- GetMinThreads - minimum number of threads the thread pool supports
- SetMaxThreads - maximum number of threads the thread pool can support
- SetMinThreads - minimum number of threads the thread pool supports
- QueueUserWorkItem - assign a piece of work to the thread pool to be executed by the next available thread
- RegisterWaitForSingleObject - Allows a callback to be issued for a specific WaitHandle when it is signalled.
- UnsafeQueueNativeOverlapped - Queue asynchronous File I/O Completion Ports using Overlapped structure.
- UnsafeQueueWorkItem - Queues work item for high performance scenarios. Does not propagate call stack or execution context information to the thread.
- UnsafeRegisterWaitForSingleObject - Allows a callback to be issued for a specific WaitHandle when it is signalled. For use in high performance scenarios - does not propagate call stack or execution context information.
Limiting number of threads in ThreadPool
Usually the number of threads in a pool is set at the optimum number. If application is constrained by the number available the limits can be changed.
Two scenarios for change - thread starvation and start-up thread speed.
If experiencing starvation (i.e. Queued work is taking too long to complete) try increasing the maximum number of threads. If application takes excessive time to start up try reducing the minimum number of threads.
ThreadPool and WaitHandle
ThreadPool provides mechanism for threads in pool to wait on WaitHandle and fire callback when WaitHandle is signalled, e.g.
Mutex mutex = new Mutex(true);
ThreadPool.RegisterWaitForSingleObject(mutex, new WaitOrTimerCallback(MutexHasFired), null, Timeout.Infinite, true);
// Signal the mutex to cause the thread to fire
mutex.Release();
static void MutexHasFired(object state, bool timedOut)
{
if (timedOut == true)...
}
SynchronizationContext class
Different environments require different threading models. Windows Forms prefer user interface code to run on main "user interface" thread. ASP.NET prefers most work to be done in thread pool. To deal with differing thread models the framework provides the SynchronizationContext class that allows code to be written without regard to the threading model of a particular application.
First get an instance of the SynchronizationContext class. Once obtained use its Send method to call code. This may (depending on environment) result in a call to another thread, but will block until the code completes. Call the Post method is a fire-and-forget model that queues up the request and returns immediately (if possible).
If executing code asynchronously is not supported (e.g. In Windows Forms threading model) then both methods will run the code and return after execution.
The methods do not return any sort of object. They are useful for executing arbitrary code, but not taking action depending on the result of the code.
SynchronizationContext ctx = SynchronizationContext.Current;
ctx.Send(RunMe, "Hi");
ctx.Post(RunMe, "Hi");
Timer Objects
Timer objects firs asynchronous call to method based on time. When creating object specify TimerCallback delegate that runs when Timer fires together with how long before timer starts firing (0 implies immediately) and interval between firings, e.g.
Timer tm = new Timer(new TimerCallback(TimerTick), null, 0, 1000);
Can change when the timer fires and its interval using the Change method.
3 timer classes:
- System.Threading.Timer - discussed above
- System.Windows.Forms.Timer - fires WM_TIMER message on same thread as the form
- System.Timers.Timer - wrapper around System.Threading.Timer allowing it to be dropped onto Visual Studio design surfaces.