A C# application runs in one or more threads that effectively execute in parallel within the same application. Here is a simple multithreaded application:
using System; using System.Threading; class ThreadTest { static void Main( ) { Thread t = new Thread(new ThreadStart(Go)); t.Start( ); Go( ); } static void Go( ) { for (char c='a'; c<='z'; c++ ) Console.Write(c); } }
In this example, a new thread object is constructed by passing it a
ThreadStart
delegate that wraps the method that
specifies where to start execution for that thread. You then start
the thread and call Go
, so two separate threads
are running Go
in parallel. However, there’s
a problem: both threads share a common resource—the console. If
you run ThreadTest
, you get output something like
this:
abcdabcdefghijklmnopqrsefghjiklmnopqrstuvwxyztuvwxyz
Thread synchronization comprises techniques for ensuring that multiple threads coordinate their access to shared resources.
C# provides the lock
statement to ensure that only one thread
at a time can access a block of code. Consider the following example:
using System; using System.Threading; class LockTest { static void Main( ) { LockTest lt = new LockTest ( ); Thread t = new Thread(new ThreadStart(lt.Go)); t.Start( ); lt.Go( ); } void Go( ) { lock(this) for ( char c='a'; c<='z'; c++) Console.Write(c); } }
Running LockTest
produces the following output:
abcdefghijklmnopqrstuvwzyzabcdefghijklmnopqrstuvwzyz
The lock
statement acquires a lock on any
reference type instance. If another thread has already acquired the
lock, the thread doesn’t continue until the other thread
relinquishes its lock on that instance.
The lock
statement is actually a syntactic
shortcut for calling the
Enter
and Exit
methods
of the BCL Monitor
class
(see Section 3.8.3):
System.Threading.Monitor.Enter(expression); try { ... } finally { System.Threading.Monitor.Exit(expression); }
In
combination
with locks, the next most common threading operations are
Pulse
and Wait
. These
operations let threads communicate with each other via a monitor that
maintains a list of threads waiting to grab an object’s lock:
using System; using System.Threading; class MonitorTest { static void Main( ) { MonitorTest mt = new MonitorTest( ); Thread t = new Thread(new ThreadStart(mt.Go)); t.Start( ); mt.Go( ); } void Go( ) { for ( char c='a'; c<='z'; c++) lock(this) { Console.Write(c); Monitor.Pulse(this); Monitor.Wait(this); } } }
Running MonitorTest
produces the following result:
aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz
The Pulse
method tells the monitor to wake up the
next thread that is waiting to get a lock on that object as soon as
the current thread has released it. The current thread typically
releases the monitor in one of two ways. First, execution may leave
the lock
statement blocked. The second way is to
call the Wait
method, which temporarily releases
the lock on an object and makes the thread fall asleep until another
thread wakes it up by pulsing the object.
The
MonitorTest
example
actually contains a type of bug called a
deadlock. When you run the program, it prints
the correct output, but then the console window locks up. This is
because there are two sleeping threads, and neither will wake the
other. The deadlock occurs because when printing
z
, each thread goes to sleep but never gets
pulsed. You can solve the problem by replacing the
Go
method with this new implementation:
void Go( ) { for ( char c='a'; c<='z'; c++) lock(this) { Console.Write(c); Monitor.Pulse(this); if (c<'z') Monitor.Wait(this); } }
In general, the danger of using locks is that two threads may both end up being blocked waiting for a resource held by the other thread. Most common deadlock situations can be avoided by ensuring that you always acquire resources in the same order.
Atomic operations
are
operations the system promises will not be interrupted. In the
previous examples, the method Go
isn’t
atomic, because it can be interrupted while it is running so another
thread can run. However, updating a variable is atomic, because the
operation is guaranteed to complete without control being passed to
another thread. The Interlocked
class provides
additional atomic operations, which allows basic operations to be
performed without requiring a lock. This can be useful, since
acquiring a lock is many times slower than a simple
atomic operation.
Much of the functionality of threads is
provided through the classes in the
System.Threading
namespace. The most basic thread
class to understand is the Monitor
class, which is
explained in the following section.
The
System.Threading.Monitor
class
provides an
implementation of Hoare’s Monitor
that allows you to use any reference-type instance as a monitor.
The Enter
and
Exit
methods, respectively, obtain and
release a lock on an object. If the object is already held by another
thread, Enter
waits until the lock is released, or
the thread is interrupted by a
ThreadInterruptedException
. Every call to
Enter
for a given object on a
thread should be matched with a call to Exit
for
the same object on the same thread.
The TryEnter
methods
are similar to the Enter
method, but
they don’t require a lock on the object to proceed. These
methods return true
if the lock is obtained, and
false
if it isn’t, optionally passing in a
timeout parameter that specifies the maximum time to wait for the
other threads to relinquish the lock.
The thread
holding a lock on an object may call one of the
Wait
methods to temporarily release the lock and
block itself, while it waits for another thread to notify it by
executing a pulse on the monitor. This approach can tell a worker
thread that there is work to perform on that object. The overloaded
versions of Wait
allow you to specify a timeout
that reactivates the thread if a pulse hasn’t arrived within
the specified duration. When the thread wakes up, it reacquires the
monitor for the object (potentially blocking until the monitor
becomes available). Wait
returns
true
if the thread is reactivated by another
thread pulsing the monitor and returns false
if
the Wait
call times out without receiving a pulse.
A
thread
holding a lock on an object may call Pulse
on that
object to wake up a blocked thread as soon as the thread calling
Pulse
has released its lock on the monitor. If
multiple threads are waiting on the same monitor,
Pulse
activates only the first in the queue
(successive calls to Pulse
wake up other waiting
threads, one per call). The PulseAll
method
successively wakes up all the
threads.
18.118.20.90