This is the second article of two parts
on dotNet threading. In this second part, I will discuss further the
synchronization objects in the System.Threading dotNet namespace, thread local
storage, COM interoperability and thread states.
Intermediate Level
This article is written for the
intermediate and senior C# developer. Working knowledge of the C# programming
language and dotNet framework is assumed. The article was written with a Beta
version of VS.NET and associated documentation. Changes, although not
anticipated, might occur before final release of VS.NET that invalidate
portions of this article.
In the first article, I discussed how to
create threads, thread pools and some of the synchronization objects available
in the System.Threading dotNet namespace. In this second article, I will
complete my discussion of the synchronization objects and will discuss thread
local storage, COM interoperability and thread states.
ReaderWriterLock
Another popular design pattern introduced
as a class in the dotNet framework is the ReaderWriterLock. This class allows
an unlimited amount of read locks or one write lock, but not both. This allows
anyone to read the protected resource, as long as nobody is writing to the protected
resource and allows only one thread to write to the protected resource at any
one time. Listing 1 presents a sample using the ReaderWriterLock class.
Listing 1:
ReaderWriterLock Class
using System;
using System.Threading;
namespace ConsoleApplication9
{
class Class1
{
public Class1()
{
rwlock = new ReaderWriterLock();
val = "Writer Sequence Number is 1";
}
private ReaderWriterLock rwlock;
private string val;
public void Reader()
{
rwlock.AcquireReaderLock(Timeout.Infinite);
Console.WriteLine("Acquired Read Handle: "
"Value = {0}", val);
Thread.Sleep(1);
Console.WriteLine("Releasing Read Handle");
rwlock.ReleaseReaderLock();
}
public void Writer()
{
rwlock.AcquireWriterLock(Timeout.Infinite);
Console.WriteLine("Acquired Write Handle");
int id = rwlock.WriterSeqNum;
Console.WriteLine("Writer Sequence Number "
"is {0}", id);
Thread.Sleep(1);
val = "Writer ";
Thread.Sleep(1);
val += "Sequence ";
Thread.Sleep(1);
val += "Number ";
Thread.Sleep(1);
val += "is ";
Thread.Sleep(1);
val += id;
Console.WriteLine("Releasing Write Handle");
rwlock.ReleaseWriterLock();
}
static void Main(string[] args)
{
Class1 obj = new Class1();
const int n = 1000;
Thread[] reader = new Thread[n];
Thread[] writer = new Thread[10];
for (int i=0;i<n;i++)
{
reader[i] = new Thread(
new ThreadStart(obj.Reader));
if (i<10)
{
writer[i] =
new Thread(
new ThreadStart(obj.Writer));
};
}
for (int i=0;i<n;i++)
{
reader[i].Start();
if (i<10)
{
writer[i].Start();
};
}
}
}
}
In the above listing, I create 10 writer
threads and 1000 reader threads. I parameterized the number of reader threads
so that I could quickly trigger different behaviors in the code by modifying
the number of reader threads. Once the threads are started they attempt to
acquire read and write lock on the ReaderWriterLock object. If you run the
code, then you can see the writer threads have a difficult time acquiring write
locks. I tried to put as many small sleep statements as I could to force the
threads to swap out of memory earlier than they would have normally.
Mutex
The last synchronization object I’ll
present here is the Mutex. The most useful feature of the Mutex class is that
it may be named. This allows you to create two Mutex objects in different areas
of code without having to share Mutex object instances. As long as the Mutex
object instances have the same name, they will synchronize with each other. You
could create the Mutex in two different processes on the same machine and the
synchronization crosses the process boundary. Nor do you have to worry about
passing the Mutex object in order to share the synchronization object between
two threads or methods (see Listing 2).
Listing 2:
Mutex Class
using System;
using System.Threading;
namespace ConsoleApplication10
{
class Class1
{
public void ThreadStart()
{
Mutex mutex = new Mutex(false, "MyMutex");
mutex.WaitOne();
Console.WriteLine("Hello");
}
static void Main(string[] args)
{
Class1 obj = new Class1();
Thread thread = new Thread(
new ThreadStart(obj.ThreadStart));
Mutex mutex = new Mutex(true, "MyMutex");
thread.Start();
Thread.Sleep(1000);
Console.WriteLine("Signal");
mutex.ReleaseMutex();
}
}
}
In the above listing, two separate Mutex
objects are created, but the Mutex class allows the two instances to interact.
The Signal will always precede the Hello in the output of this program. This is
because the Mutex in the thread is created with the lock acquired. The second
thread then creates the Mutex without acquiring the lock. The second thread
will then wait on the mutex until the main thread releases the mutex a second
later.
Thread Local Storage
The Thread class and System.Threading
namespace also contain some methods and classes for realizing thread local
storage. Thread local storage is a manner of storing data in a container that
is unique to the thread. Many threads could then use the same named container
to store their data without concern of collision. Each thread’s local storage
is distinct from another thread’s local storage and is only available in the
one thread. Listing 3 shows a small sample using the thread-local-storage
methods and classes.
Listing 3:
Thread Local Storage
using System;
using System.Threading;
namespace ConsoleApplication11
{
class Class1
{
public void ThreadStart()
{
string str1 = "My Cookie " +
Thread.CurrentThread.GetHashCode();
Console.WriteLine("worker thread: {0}",str1);
LocalDataStoreSlot lds =
Thread.GetNamedDataSlot("COOKIE");
Thread.SetData(lds, str1);
Thread.Sleep(1);
LocalDataStoreSlot lds2 =
Thread.GetNamedDataSlot("COOKIE");
string str2 = "";
str2 = (string)Thread.GetData(lds2);
Console.WriteLine("worker thread: {0}",str2);
}
static void Main(string[] args)
{
string str1 = "My Cookie " +
Thread.CurrentThread.GetHashCode();
Console.WriteLine("main thread: {0}", str1);
LocalDataStoreSlot lds =
Thread.AllocateNamedDataSlot("COOKIE");
Thread.SetData(lds, str1);
Class1 obj = new Class1();
Thread thread = new Thread(
new ThreadStart(obj.ThreadStart));
thread.Start();
Thread.Sleep(1);
LocalDataStoreSlot lds2 =
Thread.GetNamedDataSlot("COOKIE");
string str2 = "";
str2 = (string)Thread.GetData(lds2);
Console.WriteLine("main thread: {0}", str2);
}
}
}
You could also create and start more than
one thread and the behavior of the thread local storage becomes more obvious. I
have played with Win32 thread-local-storage functions and created my own for
portability to UNIX, but I have rarely found them very useful. I strongly
believe in stateless computing and thread-local-storage contradicts this
belief.
COM Interoperability
Now what about those COM apartments? How
do these new dotNet threads handle COM apartments? dotNet threads can reside in
both single and multithreaded apartments. When a dotNet thread is first started
it exists neither in a single-threaded or multithreaded apartment. A static
state variable Thread.CurrentThread.Apartment indicates the current apartment
type. If you run the code in Listing 4, then the apartment type will be
Unknown, as the thread would not have entered an apartment yet.
Listing 4:
Threading Model Attributes
using System;
using System.Threading;
namespace ConsoleApplication5
{
class Class1
{
//
line output
// // Unknown
// [STAThread] // STA
//
[MTAThread] // MTA
public static void Main(String[] args)
{
Console.WriteLine("Apartment State = {0}",
Thread.CurrentThread.ApartmentState);
}
}
}
If you uncomment the line with the
STAThread attribute, then the thread set its ApartmentState to STA. If you
uncomment the line with the MTAThread attribute, then the thread set its
ApartmentState to MTA. This allows control over the apartment type, similar to
CoInitializeEx. You can also set the ApartmentState static member directly (see
Listing 5).
Listing 5:
ApartmentState
using System;
using System.Threading;
namespace ConsoleApplication6
{
class Class1
{
static void Main(string[] args)
{
//
Thread.CurrentThread.ApartmentState =
// ApartmentState.STA;
Thread.CurrentThread.ApartmentState =
ApartmentState.MTA;
Console.WriteLine("Apartment State = {0}",
Thread.CurrentThread.ApartmentState);
}
}
}
Setting the ApartmentState property has
the same affect as using the STAThread and MTAThread attributes.
There are also class attributes that
affect the threading model used by the dotNet framework. The ThreadAffinity and
Synchronization class attributes can be used to synchronize access to a class
and its instance members.
[ThreadAffinity()]
public class Class1 : ContextBoundObject
[Synchronization()]
public class Class1 : ContextBoundObject
When calling into such classes, the calls
are serialized to limit access to the class to one thread at any one time. At
this point, these class context attributes are really thin on documentation.
So, I’ll save a more in-depth explanation that may be incorrect anyway.
Win32 to dotNet
I figured with all this work I’m doing
learning dotNet threads that I would leave you with an important resource.
Table 1 shows my attempt in converting Win32 functions to dotNet classes and
methods.
Table 1:
Converting Win32 to dotNet
|
Win32
|
dotNet
|
|
CreateEvent
|
new System.Threading.Event
|
|
CreateMutex
|
new System.Threading.Mutex
|
|
CreateSemaphore
|
n/a
|
|
CreateThread
|
new System.Threading.Thread and
new System.Threading.ThreadStart
|
|
CreateWaitableTimer
|
new System.Threading.Timer
|
|
InitializeCriticalSectiona
EnterCriticalSection
LeaveCriticalSection
DeleteCriticalSection
|
lock (C#)
System.Threading.Monitor
|
|
InterlockedCompareExchange
|
System.Threading.Interlock.CompareExchange
|
|
InterlockedDecrement
|
System.Threading.Interlock.Decrement
|
|
InterlockedExchange
|
System.Threading.Interlock.Exchange
|
|
InterlockedIncrement
|
System.Threading.Interlock.Increment
|
|
OpenEvent
|
n/a
|
|
OpenMutex
|
new System.Threading.Mutex
|
|
OpenSemaphore
|
n/a
|
|
OpenWaitableTimer
|
n/a
|
|
PulseEvent
|
n/a
|
|
ReleaseMutex
|
System.Threading.Mutex.ReleaseMutex
|
|
ReleaseSemaphore
|
n/a
|
|
ResetEvent
|
System.Threading.AutoResetEvent.Reset or
System.Threading.ManualResetEvent.Reset
|
|
ResumeThread
|
System.Threading.Thread.Resume
|
|
SetEvent
|
System.Threading.AutoResetEvent.Set or
System.Threading.ManualResetEvent.Set
|
|
SetWaitableTimer
|
n/a
|
|
Sleep
|
System.Threading.Thread.Sleep
|
|
SuspendThread
|
System.Threading.Thread.Suspend
|
|
TerminateThread
|
System.Threading.Thread.Abort
|
|
WaitForSingleObject and
WaitForSingleObjectEx
|
System.Threading.Thread.Join or
System.Threading.Monitor.Wait or
System.Threading.WaitHandle.WaitOne
|
|
WaitForMultipleObjects and
WaitForMultipleObjects
|
System.Threading.WaitHandle.WaitAll or
System.Threading.WaitHandle.WaitAny
|
If you were to undertake a project of
converting a Win32 application to a dotNet application, then this table could
prove very useful. In some cases, a few objects and methods in the dotNet
framework could closely emulate a Win32 function. I had to, on occasion, decide
how closely they matched and sometimes decided that a match was not
appropriate. As an example, you could create a semaphore with a Mutex object
and a counter. But I wouldn’t say it’s a close match, so I didn’t mention these
instances. In other cases, I had to decide between two matches.
Thread States
The last few topics in this article are
really just the few bits of reference information I dug up on dotNet threads.
This section describes the states of a thread. The Thread object in the dotNet
framework has a property called the ThreadState, which is one of the members of
the following enumeration, which I pulled from the dotNET documentation.
public enum ThreadState
{
Running = 0,
SuspendRequested = 2,
Background = 4,
Unstarted = 8,
WaitSleepJoin = 32,
Suspended = 64,
AbortRequested = 128,
Aborted = 256
};
Unfortunately, I have been able to
generate ThreadState’s that are not in this enumeration. Specifically, the
Stopped ThreadState seems to be missing and is easy to generate. If you check
the state of a thread that has run to completion, then the state is marked as
Stopped.
What I also found is that it is quite easy to generate dual states.
You can be in the AbortRequested state and the WaitSleepJoin state. If you
catch the ThreadAbortException and then call Thread.Sleep, then the ThreadState
will be “WaitSleepJoin, AbortRequested”, a dual state. The same is true if you
are sleeping when the Suspend instance method is called. Immediately after the
call to the Suspend instance method, the ThreadState property reports
“SuspendRequested, WaitSleepJoin”, then quickly changes to “WaitSleepJoin,
Suspended”.
I’ve encountered a few state diagrams
that tried to depict the state transitions of dotNet threads. I must say that
most are misleading or incomplete. The biggest problem is that most of the
state diagrams did not attempt to account for dual states. My own attempt at
the state diagram, I know, is still lacking but much further along then
anything else I’ve seen (see Figure 1).
Background Threads
There is still a lot missing from the
state diagram. Specifically, what happens when you Suspend(), Wait(), Join(),
Sleep(), Abort() a background thread. I’m not going to confuse the diagram to
explain these new states. Rather, let me explain that a thread is either a
background thread or a foreground thread. Actions on a background thread are
equivalent to actions on a foreground thread, except in one respect, which I
will explain in the next paragraph. So, if you attempt to suspend a running
background thread, then it will move to the SuspendRequested state, then to the
Suspended state and finally back to the Background state, in the same manner as
a foreground thread.
The difference between a background
thread and a foreground thread is pretty simple. When the last foreground
thread of a process is stopped, then the process terminates. There could be
zero, 1 or an infinite number of background threads and they have no vote in
whether a process terminates or not. So when the last foreground thread stops,
then all background threads are also stopped and the process is stopped.
I’ve seen quite a few dot-NET programmers
incorrectly use the background thread to mean any thread created using the
Thread constructor. The terminology is therefore getting very confusing. The
correct meaning of background thread in dotNet framework is a thread that does
not have impact on whether a process is terminated.
Thread Safe Objects and Types
Here’s a rather interesting tidbit of
news. Many of the dotNet objects and types are thread-safe. The first time I
heard that I was rather confused at what it could mean. Does this mean an
increment (++) operation on a C# integer is atomic? I put together a small
piece of C# code that launched a thousand threads and incremented and
decremented one integer a million times per thread. I structured the code to
swap the threads like mad to try and create a race condition that would
invalidate the operations on the integer. I was unsuccessful in generating
incorrect results. So, I assume the operation is atomic. But I don’t have any
proof (beyond proof-by-example) that it is an atomic operation.
Interlocked
Throughout this article, I have written
code that assumes that some operations on C# objects and types are atomic. I
would never suggest writing such code in a production environment. In such an
environment, you will have to fall back onto our old InterlockedIncrement and
InterlockedDecrement friends. In C#, these are in the
System.Threading.Interlocked class. The class has two static methods
Interlocked.Increment and Interlocked.Decrement. Use them well.
Conclusion
I started this trek into dotNet threads
for one reason. I wanted to evaluate them as a possible alternative for servers
that require a lot of thread programming. What I found was that dotNet’s
Threading namespace is by far the easiest way to write applications that
require a lot of thread programming. I didn’t find any performance problems
with the dotNet threads, but neither did I find them any faster than other
thread libraries available in C++ or Java threads.
About the Author
Randy Charles Morin is the Lead Architect
of SportMarkets Development from Toronto, Ontario, Canada and lives with his
wife and two kids in Brampton, Ontario. He is the author of the www.kbcafe.com website, author of Wiley’s Programming
Windows Services book and co-author of many other programming books and
articles.
Copyright 2002-03 Randy Charles Morin