Documente Academic
Documente Profesional
Documente Cultură
Fundamentals of Concurrent
Programming for .NET
Greg Beech (greg.beech@charteris.com)
18 March 2005
When the currently running thread is paused and another thread is allowed to run, it is known as a
context switch. Although context switches are required to allow multiple threads to run on a single
processor they do have some performance cost such as saving and restoring registers. As such it is not
a good idea to have too many threads as otherwise the system will spend more time context switching
and less time actually executing the threads. As a rough guideline, under normal conditions there may
be a few thousand context switches per second – this can be monitored with the performance counter
Thread\Context Switches/sec using a tool such as Microsoft System Monitor.
On a hyperthreaded CPU two threads can be executed simultaneously which can help to reduce the
number of context switches, however there are certain compromises as it still uses the same core, so
the speed increase is not as much as you might expect with a typical increase being up to around 25%.
A multi-processor system or multi-core processor can execute multiple threads truly in parallel and
independently of each other so the performance can theoretically increase in proportion to the number
of processors/cores. In reality this is not quite true because of the requirement for synchronization
between the processors in both hardware and software – and the fact that it is hard to write systems
that effectively use this many threads simultaneously! Many server applications see negligible increases
in speed past eight or even four processors.
2.2 Fibers
A fiber can be thought of as a lightweight thread that it is not scheduled by Windows, but must be
manually scheduled by an application to run against a real OS thread. The Common Language Runtime
(CLR) hosting API in versions 1.0 and 1.1 had basic support for fiber scheduling, and in version 2.0
and later these have been comprehensively overhauled, essentially to allow the CLR to be hosted in
SQL Server 2005 in fiber mode. Sophisticated hosts such as SQL Server can use fibers to improve
performance, but unless the scheduling algorithm is extremely well tuned the performance will
probably be lower than not using fibers. Unless you really need the last 10% of performance from a
system, and you have a lot of time available, fibers are not something that you should be overly
concerned with other than just to be aware of their existence.
2.3 Managed threads
A managed thread is represented by the System.Threading.Thread class. In the .NET Framework
1.0 and 1.1 managed threads map directly onto an OS thread, however this is not guaranteed to be the
case in the future, particularly when hosted in an environment such as SQL Server 2005 in fiber mode.
Note that the .NET Framework also has the System.Diagnostics.ProcessThread class which
represents an OS thread although this is not particularly useful for multi-threaded programming. There
is no relationship between the Thread and ProcessThread classes and you cannot get an instance of
one from the other.
Every one of the main .NET languages can create and control managed threads explicitly, for example:
The problem here is that the method first waits for one web service to complete, and then calls the
other one – each of these may take a couple of seconds to do the lookup and return the information.
This method can be significantly enhanced by using the asynchronous versions to call the web services
in parallel, e.g.
void DisplaySummary(int customerId)
{
//start the lookups asynchronously
LookupService svc = new LookupService();
IAsyncResult custResult = svc.BeginGetCustomerData(customerId, null, null);
IAsyncResult orderResult = svc.BeginGetOrderData(customerId, null, null);
The code is very similar to the web method invocation, except that here you are defining and creating
instances of delegates which reference the methods, and then run those delegates asynchronously. The
BeginInvoke and EndInvoke methods on the delegate instance are created by the compiler and as
such have a method signature that matches the synchronous method, with additional parameters added
for a callback and a state obect.
Using this technique you can invoke any method asynchronously, though bear in mind that it is slower
to execute a method asynchronously so it is only worth doing if the concurrency gains outweigh the
overhead incurred.
2.6 Events and multicast delegates
One of the common misconceptions about events is that they give you concurrency and that the
handlers run in the managed thread pool. Events run sequentially and on the same thread that raised
the event. This is because underneath an event is really just a wrapper around an instance of the
System.MulticastDelegate class and as such is subject to its behaviour.
A multicast delegate is a delegate that allows multiple handlers to be registered. When the delegate is
invoked, all of the handlers are executed in the order that they were registered. The EventArgs object
that is passed to the handlers may be modified, and the modified version will be received by the next
handler in the chain (for this reason it is a good idea to make your EventArgs classes immutable
unless you want this to occur).
The following code demonstrates the pattern of event execution when a number of handlers are
registered to receive the event:
class EventClass
{
event MyEventHandler TestEvent;
Console.WriteLine(
"Method: thread {0}, count {1}",
Thread.CurrentThread.GetHashCode(),
e.Count);
}
To add the item there are two changes to the class state required: the object must be inserted into the
array of items and the count must be incremented. If two threads could execute this code at the same
time then a possible sequence of events might be as follows:
1. Thread “A” gets the index and stores the item.
2. A context switch occurs to let thread “B” run.
3. Thread “B” gets the index and stores the item.
4. Thread “B” increments the count and returns the index.
5. A context switch occurs to let thread “A” run.
6. Thread “A” increments the count and returns the index.
In this case, both threads store their items at the same index in the array, so the item that was written
by thread “A” is lost. In addition, both threads increment the count so now the count is actually one
more than the number of items in the collection. To make this method thread-safe we could rewrite it
as follows using the Monitor class:
public int Add(object item)
{
int index;
Monitor.Enter(this.syncRoot);
try
{
index = this.count;
this.items[index] = item;
this.count++;
}
finally
{
Monitor.Exit(this.syncRoot);
}
return index;
}
The code between Monitor.Enter and Monitor.Exit is a critical section, enclosed by a lock on the
private object syncRoot. There are a couple of important points in this code:
♦ The lock is taken on a private object, not a publicly visible object such as the class itself. It is quite
common to see statements such as lock(this) in code, which can lead to major problems if
another (possibly totally unrelated) class decides also to lock on the class instance. Even worse is
locking on types, e.g lock(typeof(MyCollection)), which is commonly used for static methods
– types are app-domain agile so you may be inadvertently locking a type in another app-domain!
♦ The lock is released in a finally block to ensure that even under error conditions it is released. If
the lock was not released then no other thread could ever enter this method.
It is important to note that it is not only the Add method that must be synchronized – the collection is
also likely to have other operations such as a Count property, an indexer, and IndexOf and Remove
methods, each of which need to be synchronized. When synchronizing each of these the same lock
object must be used as otherwise although the Add method would be synchronized the Remove
method could alter the state in parallel. Correctly synchronized IndexOf and Remove methods are
shown below:
public int IndexOf(object item)
{
int index = -1;
lock (this.syncRoot)
{
for (int i = 0; i < this.count; i++)
{
if (object.Equals(item, this.items[i]))
{
index = i;
break;
}
}
}
return index;
}
These methods lead to a final interesting point about the Monitor class. Note that when Remove is
called it takes a lock, and then calls IndexOf, which also takes a lock on the same object. This works
because a thread is allowed to lock the same object multiple times; it is only other threads that cannot
acquire the lock. Note though that the thread must release the lock as many times as it acquires it
because the .NET Framework tracks the number of time a lock has been taken on an object, not just
whether it is locked.
3.4 MethodImplOptions.Synchronized
The .NET Framework includes the System.Runtime.CompilerServices.MethodImplAttribute
attribute, which can be used with the enumeration value MethodImplOptions.Synchronized to
indicate to the compiler that access to the entire method should be synchronized. This is, in effect, a
declarative way to use the Monitor class to synchronize access to the method without any need for
imperative programming, e.g.
There is no requirement in the ECMA specification for this behaviour; my assumption is that it is done
as a reasonable compromise for the concise syntax as it is better to lock on the class type or instance
than nothing in a multi-threaded environment. The good news is that if you want to be really safe you
can explicitly code your own methods to add and remove handlers (which will not automatically be
made synchronized) and lock on a private object in those:
private readonly object syncRoot = new object();
private EventHandler myEvent;
3.5 ReaderWriterLock
For objects which store state that is retrieved more often that it is written to, such as a Hashtable
mainly used for lookups, the Monitor class may not be the most efficient way of controlling access.
The System.Threading.ReaderWriterLock class allows multiple threads to read state concurrently,
or a single thread to write state. For example, the previously considered Add and IndexOf methods
could be synchronized with a ReaderWriterLock as shown below:
public class MyCollection
{
private readonly ReaderWriterLock rwlock = new ReaderWriterLock();
private int count = 0;
private object[] items = new object[16];
As when using the Monitor class, the lock is released in a finally block to ensure that even under
error conditions it is always released. In this collection either multiple threads can retrieve the index of
an item simultaneously, or exactly one thread can add an item. Note that I have used a memory barrier
in the Add method to ensure that all writes occur before the writer lock is released as
ReaderWriterLock does not provide the same guarantees about writes not crossing the lock
boundary as Monitor.
Note that reader and writer threads waiting for the lock are queued separately. When a writer lock is
released, all queued reader threads at that instant are granted reader locks. When all of those reader
locks have been released the next writer thread in the writer queue is granted a writer lock. This
alternating behaviour between a collection of readers and a single writer ensures fairness in accessing
the resource, and that neither reader nor writer threads are starved of access.
3.6 Wait handles
A wait handle is a synchronization object that uses signalling to control the progress of threads rather
than a lock object. The .NET Framework 1.0 and 1.1 provide three types of wait handle in the
System.Threading namespace – Mutex, ManualResetEvent and AutoResetEvent – all of which
derive from the abstract base class System.Threading.WaitHandle. Version 2.0 also provides a
Semaphore class; if you do not have access to this then I have provided a code listing for a semaphore
in Appendix B which has a similar public interface and behaviour.
All wait handles have a commonality in that you can call the instance method WaitOne to wait for that
particular instance, or the WaitHandle class has static methods WaitAll and WaitAny to wait for all
or any of a number of wait handles respectively – note that the wait handles may be different types.
The different behaviours of the wait handles will be discussed in the context of the instance method
WaitOne ; the behaviour of the static methods can be inferred from this.
Finally a word of warning – many articles I have read use these events to attempt to protect a critical
section where different threads read and write a shared resource. The general pattern is that they call
Reset on the writer thread and then start changing the shared resource believing that it is protected as
the reader threads will no longer be using it. However, reader threads that called WaitOne just before
the Reset may already be using the resource, and calling Reset on the event will have no effect on
them, so there may still be threads still using the resource while you change it! If you are trying to
protect a critical section do not use ManualResetEvent or AutoResetEvent, use one of the objects
designed for that purpose such as Monitor, ReaderWriterLock or Mutex.
3.7 Interlocked
The System.Threading.Interlocked class is not designed to protect critical sections, but is for
simple atomic operations such as incrementing/decrementing values and exchanging the values of
items. Note that the results of all methods on the Intelocked class are immediately visible on all
threads on all processors, so no lock or memory barrier is needed.
3.7.1 Increment and Decrement
These are used simply to increment or decrement a value in an atomic and thread-safe manner; in
addition it returns the previous value of the item before the increment or decrement occurred. The
following code shows an example:
Note that the usefulness of this method is somewhat limited by the fact that the original object is
passed as a reference parameter and therefore must be exactly of type int, float or object (the three
types allowed by overloads of the method in .NET 1.0 and 1.1), it cannot be a type derived from them.
The following code will fail to compile with error CS1503 (cannot convert type ‘ref MyObject’ to ‘ref
object’).
MyObject original = new MyObject();
MyObject replacement = new MyObject();
MyObject previous = (MyObject)Interlocked.Exchange(ref original, replacement);
The CompareExchange method is very similar to Exchange, except it also allows you to compare the
item to another to see if the exchange should occur, also as an atomic operation. If the value already
stored does not match the comparand the exchange does not happen. The following code only
changes the original value if it is equal to 1234:
int original = 1234;
int comparand = 1234;
int replacement = 5432;
int previous = Interlocked.CompareExchange(ref original, replacement, comparand);
//do something with the previous value
Again the CompareExchange method is limited by the fact that the original object is passed as a
reference parameter. It would seem logical to include generic versions of these methods in .NET 2.0
and later (i.e. Interlocked.Exchange<T> and Interlocked.CompareExchange<T> ) but they are not
evident at the time of writing.
3.8 Summary
The .NET Framework provides many different ways to synchronize threads including basic locks,
attributes, reader/writer locks, mutexes, signalled events, semaphores and interlocked operations. Each
has its benefits, drawbacks and limitations so it is important to choose the correct mechanism to suit
each place in your application. Concurrency in .NET is made more difficult by the weak memory
model; one of the consequences of it is that only Monitor and Interlocked guarantee to make the
results of any changes made to shared resource visible to all other threads, so with any other
synchronization method you must use a memory barrier to manually achieve this effect before
releasing the lock.
void CopyThreadStart()
{
int i;
while ((i = Interlocked.Increment(ref this.index)) < this.files.Length)
{
//copy the file at the index i in the array of files
}
}
}
In some cases such as this sample it may not matter that the execution is nondeterministic however in
other cases the output may have to be deterministic – for example if this application split each file into
sections and used multiple threads per file then it would be important that the sections of the file were
reassembled in the same order!
The overall execution of any multi-threaded application will be nondeterministic because it depends on
factors outside the control of the programmer such as thread scheduling and often user input or
communication with external applications. Nonetheless, it is important to understand whether any
portions of the application do require deterministic execution and use appropriate means to ensure
this; the easiest way is to write that portion as single threaded but if it is in a performance critical area
then more involved approaches may be required.
4.2 Deadlocks
A deadlock is a condition where two or more threads are blocked, each waiting for the other to take
some action. Deadlocks arise primarily as a result of a single shared resource having two or more locks
that protect it and where two threads have each acquired one of the locks and are waiting for the other
thread to release the other lock so they can continue. The following code shows a class where this
situation could arise.
This class has two data structures, a binary tree and a hashtable, and provides methods to add and
remove an item from them. The Add method first locks tree then table, whereas the Remove method
locks table then tree. If two threads were to execute these methods simultaneously then Add could
lock tree and Remove could lock table; at this point each thread would be waiting for the other to
release the resource it is trying to lock on and neither thread can continue – this is a deadlock.
In the above example it is quite easy to see how the deadlock can arise and the fix is also simple, just
change both methods to always lock the objects in the same order and then no deadlock can arise.
Unfortunately it isn’t always this simple to find the issue: sometimes locks are taken in different
methods and held while other methods are called, for example:
class Table
{
private readonly object syncRoot = new object();
private View view;
class View
{
private readonly object syncRoot = new object();
private Table table;
If the thread is aborted and the exception is thrown where indicated, then the Monitor.Exit method
will not execute and the lock will not be released. Unfortunately as the lock can only be released by the
thread that acquired it, which has just been aborted, this lock will never be released and any thread that
calls the Add method will block indefinitely.
Fortunately there is generally no need to use the Suspend, Resume and Abort methods. Almost any
thread synchronization that can be done with them can be done with wait handles, although it
admittedly does need cooperation between the running threads rather than being controlled by just one
void ProcessNextItem()
{
if (syncQueue.Count > 0)
{
object item = syncQueue.Dequeue();
//do something with the item
}
}
If two threads call ProcessNextItem concurrently then the following sequence could occur:
1. Thread “A” checks the count, it is 1 and so it enters the if block.
2. A context switch occurs to let thread “B” run.
3. Thread “B” checks the count, it is 1 and so it enters the if block.
4. Thread “B” dequeues the item and processes it.
5. A context switch occurs to let thread “A” run.
6. Thread “A” tries to dequeue the item but throws an InvalidOperationException because
the queue is empty.
This is a particularly nasty type of bug because it is very hard to reproduce, so may not be discovered
until production. It is also conceptually hard to debug because the developer is operating under the
impression that the queue is thread-safe, so how can it be subject to threading problems?
As a point of interest, new collections such as generics introduced in .NET 2.0 do not have the design
pattern to allow synchronized collections because this type of bug has been introduced so commonly
as a result of it. There is also the additional drawback that the methods/properties on the collection
have to be virtual and therefore suffer from slower calling due to v-table lookups, and the contents of
the methods cannot be inlined by the JIT compiler.
The queue example illustrates why placement of critical sections is so important if you are to avoid
synchronization problems. They need to be localized enough in the code so that they are not held for
excessive periods of time and are not synchronizing code that does not need it, but they also need to
be at a high enough level that they have an awareness of the circumstances under which they are
operating – putting a critical section in the Dequeue method is not high level enough as it is unaware
that it is being called based on the value of the Count property. The code can be rewritten to be truly
thread-safe by using manual locking as follows:
Queue queue = new Queue();
void ProcessNextItem()
{
bool dequeued = false;
object item = null;
lock (queue)
{
if (queue.Count > 0)
{
item = queue.Dequeue();
dequeued = true;
}
}
if (dequeued)
{
//do something with the item
}
}
Note that here the item variable is declared outside the lock statement so that the lock only needs to
be held for the period of time it takes to check the count and dequeue the item; any processing on the
item is not synchronized so concurrency will be increased.
private MyObject()
{
this.config = this.LoadConfiguration();
}
private MyObject()
{
this.config = this.LoadConfiguration();
}
class MyConnection
{
event MyEventHandler Opened;
4.7 Summary
This section has highlighted some of the key design considerations when writing concurrent
applications. You must be aware that the overall execution of the application will be nondeterministic
and that this could lead to deadlocks if the locking strategy is not well planned; in addition, it is
important to ensure that any synchronization code is correctly placed. The reasons for not suspending
or aborting threads has been highlighted, and a couple of common issues relating to the double-lock
pattern and raising of events have been discussed – both of these are written incorrectly in the vast
majority of code, but fortunately both have low-impact and prescriptive fixes.
public Semaphore(
int initialCount, int maximumCount, string name, out bool createdNew)
{
this.Initialize(initialCount, maximumCount, name, out createdNew);
}
[SuppressUnmanagedCodeSecurity]
private sealed class NativeMethods
{
internal const int ERROR_ALREADY_EXISTS = 183;
internal const uint SYNCHRONIZE = 0x00100000;