Introduction
STA (Single Threaded Apartment) model requires that all access to an object be done by its creation thread and that any thread can create the object. Other threads need to marshal their calls to the object to the creation thread. The base class Control provides several methods (Invoke, BeginInvoke, EndInvoke) for this purpose.
Windows Forms uses STA because they’re built on the underlying Win32 objects that are STA. Calling into an STA object from an MTA thread (or the opposite) is allowed but usually SLOW.
Most of the .NET Framework is free threaded (each class defines its own threading issues and mechanisms are available to write compatible multi-threaded code).
Execution Units
Process
Win32 process.
System.Diagnostics.Process.GetCurrentProcess().Threads
Returns a collection of the threads in the current process.
AppDomain
Because of verifiable type-safety, provides process like isolation at lower performance cost.
Responsible for loading & unloading assemblies and security.
Every thread runs in a single AppDomain. Threads can transfer between AppDomains.
Objects can be copied between AppDomains or accessed by proxy.
System.AppDomain.CurrentDomain == System.Threading.Thread.GetDomain.
Most interesting methods: CreateDomain, SetData, GetData.
Thread
For a clean shutdown abort foreground threads and then Join them to wait for them to exit.
ProcessorAffinity is a cpu bitmask.
Deadlock Avoidance Rules:
1. Acquire resources in fixed sequence.
2. Release resources in the inverse sequence.
3. Don’t wait for anything indefinitely.
4. Acquire resources as late as possible.
5. When an attempt to acquire a resource fails, release any dependent resources and reschedule.
new Thread(new ThreadStart(method))
ApartmentState
enum AppartmentState { MTA, STA, Unknown}.
Can only be set before the first call to unmanaged code.
ThreadPriority
enum ThreadPriority { AboveNormal, BelowNormal, Highest, Lowest, Normal }
ThreadState
[Flags] enum ThreadState { Aborted, AbortRequested, Background, Running, Stopped, StopRequested, Suspended, SuspendRequested, Unstarted, WaitSleepJoin }.
IsAlive
true if thread has been started and has not terminated normally or aborted.
IsBackground
Guarantees thread cleanup after all non-IsBackground threads have exited.
IsThreadPoolThread
Thread is managed by the ThreadPool. Name can’t be set.
Name
User friendly thread name property.
void Abort(object state)
Raises the ThreadAbortException in the thread on which it is invoked. The state will be available through the exception’s ExceptionState property. Abort can be canceled by calling void ResetAbort()
void Interrupt()
Interrupts a thread that is in the WaitSleepJoin state. Will wait until a thread is next in the WaitSleepJoin state before interrupting.
trueIfTerminated = bool Join(TimeSpan)
Waits for the thread to terminate or the timeout.
void Resume()
Resumes a thread that has been Suspend()’ed.
void Sleep(TimeSpan)
Blocks thread for a specified time. ThreadState adds WaitSleepJoin.
void Start()
Changes the ThreadState to Running. Begins executing the method bound to the ThreadStart delegate.
void Suspend()
Changes the ThreadState to Suspended.
AllocateNamedDataSlot
GetNamedDataSlot
Concurrency Control / Thread Safety
Monitor
Contains a reference to the thread that holds the lock, a ready queue of threads ready to obtain the lock, and a waiting queue of threads waiting for notification of a change in the state of the locked object.
Much faster than a Mutex (100x).
void Enter(object)
Any reference type can be used as the object to lock. For static methods use the classes Type reference. Locking only applies to clients using the exact same object reference.
Thread will wait if lock is already held by another thread.
Enter calls must be balanced by Exit calls on every thread. Place the corresponding Exit call in a finally block.
trueIfLockObtained = bool TryEnter(object, TimeSpan)
Tries for a fixed period of time to acquire a lock.
trueIfPulsed = bool Wait(object, TimeSpan, bool yieldContext)
Can only be called by the thread that holds the lock on object.
Lock is automatically released for the duration of the call.
If the timeout expires before enough Pulse or PulseAll calls occur to move the waiting thread from the waiting queue to the ready queue, it is moved to the back of the ready queue and will receive a false return value. The timeout is therefore not a tight control over how long it may take to regain control.
void Pulse(object)
Can only be called by the thread that holds the lock on object.
Moves the thread at the front of the waiting queue to the back of the ready queue.
void PulseAll(object)
Can only be called by the thread that holds the lock on object.
Moves all threads in the waiting queue to the back of the ready queue.
void Exit(object)
Decrements the lock count on object. Object must match what was passed to Enter. Thread continues to hold lock until all Enters have been matched by Exits.
When lock is released the thread at the front of the ready queue acquires the lock.
[System.Runtime.CompilerServices.MethodImpl(Synchronized)]
Method attribute. Compiler puts a monitor on an entire method.
volatile
The compiler will not reorder access to the variable and always read its value from memory (not a register).
Volatile variables can’t be passed by reference.
Only class fields can be volatile.
WaitHandle
exitedContextBeforeWait = bool WaitAll(WaitHandle[], TimeSpan, bool yieldContext)
arrayIndexOrWaitTimeout = int WaitAny(WaitHandle[], TimeSpan, bool yieldContext)
trueIfSignalFalseIfTimeout = WaitOne(TimeSpan, bool yieldContext)
Mutex
Mutex(bool initiallyOwned, string nameOrNull, out bool createdNew)
void ReleaseMutex()
Must call same number of times as the number of wait calls.
Signaled when unowned and signaled for owner.
When a thread terminates, owned mutexes are released.
Works between processes. Much slower than Monitor, AutoResetEvent, and ManualResetEvent.
AutoResetEvent
AutoResetEvent(bool trueForInitiallySignaled)
trueIfSucceeded = bool Set()
When set, remains signaled until a thread is released by this WaitHandle, then automatically reset.
ManualResetEvent
ManualResetEvent(bool trueForInitiallySignaled)
trueIfSucceeded = bool Set()
trueIfSucceeded = bool Reset()
Interlocked
originalLocation = object CompareExchange(ref object location, object newValueIfLocationEqualsComparand, object comparand)
originalLocation = object Exchange(ref object location, object newValue)
decrementedValue = int Decrement(ref int location)
incrementedValue = int Increment(ref int location)
ContextBoundObject
Supports grouping objects by “context” and then enforcing concurrency constraints on them.
Access to an object derived from ContextBoundObject from a different context go through a proxy.
Non-ContextBoundObjects are called agile objects.
attribute can be placed on classes derived from ContextBoundObject to force all access from different contexts to go through a synchronization proxy.
High Level Constructs
lock (object) statement
Typically object is “this” for instances, “typeof(class)” for statics and all instances.
Equivalent to:
System.Threading.Monitor.Enter(object); try { statement } finally { System.Threading.Monitor.Exit(object); }
delegate
delegate myReturnType AnyDelegate(object anyArgs)
IAsyncResult BeginInvoke(object anyArgs, AsyncCallback, object state)
myReturnType EndInvoke([only non-in-only args,] IAsyncResult)
Every delegate class receives three compiler generated methods: Invoke, BeginInvoke, EndInvoke. Invoke is the composition of BeginInvoke & EndInvoke.
Implemented using ThreadPool(?).
ThreadPool
static bool System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback callback, object state)
delegate void System.Threading.WaitCallback(object state)
This function and delegate work as follows:
1. Construct a WaitCallback delegate from a callback method with a void(object state) signature.
2. Queue the delegate and optionally pass in an object which the callback will receive.
3. The callback method is invoked by a thread from the ThreadPool.
4. Your on your own in terms of letting the caller know the work is done as well as aborting if it takes to long.
5. The call can fail (in which case false is returned).
Unsafe version is faster because it doesn’t copy the stack to the callback which can be a security issue.
AutoResetEvent ev = new AutoResetEvent(false) ? WaitHandle
delegate void WaitOrTimerCallback(object, bool)
static RegisteredWaitHandle ThreadPool.RegisterWaitForSingleObject(WaitHandle, WaitOrTimerCallback, object, int, bool)
RegisteredWaitHandle.Unregister(WaitHandle)
AutoResetEvent.Set()
Each time the RegisteredWaitHandle is signaled, the WaitOrTimerCallback method will be invoked on a ThreadPool thread.
Multiple threads will be used to call the callback if the WaitHandle is signaled again before earlier invocations complete.
In additions, a timer is kept running and will also invoke the callback if it expires. The timer is reset by the WaitHandle being signaled.
RegisterWaitForSingleObject differs from QueueUserWorkItem in the following significant ways:
Work gets done by signaling a WaitHandle rather than by calling QueueUserWorkItem.
A timer runs automatically and also invokes the callback if it expires.
All ThreadPool threads are MTA.
Containers
Hashtable
Thread-safe for multiple readers and one writer.
ArrayList
Thread-safe for multiple readers.
Queue
NOT thread-safe.
Queue q = Queue.Synchronized(new Queue());
Makes a thread-safe wrapper for a queue.
2.5 times slower.
Returns a private derived queue wrapper class for a given queue.
Uses Monitor to guard the wrapped interface of the object returned by queue.SyncRoot (which is just the original queue reference).
ReaderWriterLock
ReaderWriterLock()
Creates an object used to guard a shared resource.
Logical implementation is a queue of waiting readers, a list of active readers, a queue of waiting writers and an active writer.
There’s no explicit connection to the shared resource.
void AcquireReaderLock(TimeSpan)
void AcquireWriterLock(TimeSpan)
Blocks until the requested lock is granted.
Throws ApplicationException on timeout.
void ReleaseReaderLock()
void ReleaseWriterLock()
Acquires must be balanced by releases.
Use a finally clause.
LockCookie UpgradeToWriterLock(TimeSpan)
void DowngradeFromWriterLock(ref LockCookie)
More efficient than releasing a reader lock and acquiring a writer lock.
The LockCookie restores the number of ReaderLocks held before the upgrade.
Don’t use a ReleaseLock() cookie.
LockCookie ReleaseLock()
void RestoreLock(ref LockCookie)
Efficient way to release all the locks held and easy to recover the same locks.
Don’t use an UpgradeToWriterLock() cookie.
bool IsReaderLockHeld
bool IsWriterLockHeld
For the curious.
int WriterSeqNum
bool AnyWriterSince(int)
Used to verify the validity of a cached resource value.
Must be holding a reader lock.
Thread Safety in .NET Libraries
All public static methods are thread-safe.
Common thread-safe classes:
· System.Console
· System.Text.Encoding
· System.Diagnostics.Debug
· System.Diagnostics.Trace
· System.Diagnostics.PerformanceCounter