|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Table of ContentsIntroductionThis short article is about the three less well understood methods of the These methods provide low-level synchronization between threads. They are the most versatile constructs, but they are harder to use correctly than other synchronization primitives like Basicspublic partial static class Monitor
{
public static bool Wait( object o ) { ... } // and overloads
public static void Pulse( object o ) { ... }
public static void PulseAll( object o ) { ... }
}
At the most basic level, a thread calls There is a requirement that all these methods must be called from within a For example: readonly object key = new object();
// thread A
lock ( key ) Monitor.Wait( key );
// thread B
lock ( key ) Monitor.Pulse( key );
If thread A runs first, it acquires the lock and executes the LocksYou may have noticed a little problem with the code above. If thread A holds the lock on the
QueuesThe ready queue is the collection of threads that are waiting for a particular lock. The
Recommended patternThese queues can lead to unexpected behaviour. When a The solution is to use a readonly object key = new object();
bool block = true;
// thread A
lock ( key )
{
while ( block )
Monitor.Wait( key );
block = true;
}
// thread B
lock ( key )
{
block = false;
Monitor.Pulse( key );
}
This pattern shows the reason for the rule that locks must be used: they protect the condition variable from concurrent access. Locks are also memory barriers, so you do not have to declare the condition variables as The Secondly, it solves the problem of the queues. If thread A is pulsed, it leaves the waiting queue and is added to the ready queue. If, however, a different thread acquires the lock and this thread sets UsesThe code above is actually an implementation of an So, the rule of thumb is: use higher level primitives if they fit. If you need finer control, use the TipsIt is usually okay to use There are overloads of ExampleHere is an example with full source code that demonstrates the versatility of this pattern. It implements a blocking queue that can be stopped. A blocking queue is a fixed-size queue. If the queue is full, attempts to add an item will block. If the queue is empty, attempts to remove an item will block. When This is a fairly complex set of conditions. You could implement this using a combination of higher-level constructs, but it would be harder. The pattern makes this implementation relatively trivial. class BlockingQueue<T>
{
readonly int _Size = 0;
readonly Queue<T> _Queue = new Queue<T>();
readonly object _Key = new object();
bool _Quit = false;
public BlockingQueue( int size )
{
_Size = size;
}
public void Quit()
{
lock ( _Key )
{
_Quit = true;
Monitor.PulseAll( _Key );
}
}
public bool Enqueue( T t )
{
lock ( _Key )
{
while ( !_Quit && _Queue.Count >= _Size ) Monitor.Wait( _Key );
if ( _Quit ) return false;
_Queue.Enqueue( t );
Monitor.PulseAll( _Key );
}
return true;
}
public bool Dequeue( out T t )
{
t = default( T );
lock ( _Key )
{
while ( !_Quit && _Queue.Count == 0 ) Monitor.Wait( _Key );
if ( _Queue.Count == 0 ) return false;
t = _Queue.Dequeue();
Monitor.PulseAll( _Key );
}
return true;
}
}
This implementation is safe for concurrent access by an arbitrary number of producers and consumers. Here is an example with one fast producer and two slow consumers: internal static void Test()
{
var q = new BlockingQueue<int>( 4 );
// Producer
new Thread( () =>
{
for ( int x = 0 ; ; x++ )
{
if ( !q.Enqueue( x ) ) break;
Trace.WriteLine( x.ToString( "0000" ) + " >" );
}
Trace.WriteLine( "Producer quitting" );
} ).Start();
// Consumers
for ( int i = 0 ; i < 2 ; i++ )
{
new Thread( () =>
{
for ( ; ; )
{
Thread.Sleep( 100 );
int x = 0;
if ( !q.Dequeue( out x ) ) break;
Trace.WriteLine( " < " + x.ToString( "0000" ) );
}
Trace.WriteLine( "Consumer quitting" );
} ).Start();
}
Thread.Sleep( 1000 );
Trace.WriteLine( "Quitting" );
q.Quit();
}
And, here is the output of one run: 0.00000000 0000 >
0.00006564 0001 >
0.00009096 0002 >
0.00011540 0003 >
0.09100076 < 0000
0.09105981 < 0001
0.09118936 0004 >
0.09121715 0005 >
0.19127709 < 0002
0.19138214 0006 >
0.19141905 0007 >
0.19156006 < 0003
0.29184034 < 0004
0.29195839 < 0005
0.29209006 0008 >
0.29211268 0009 >
0.39240077 < 0006
0.39249521 < 0007
0.39265713 0010 >
0.39268187 0011 >
0.49300483 < 0008
0.49308145 0012 >
0.49310759 0013 >
0.49324051 < 0009
0.59353358 < 0010
0.59361452 < 0011
0.59378797 0014 >
0.59381104 0015 >
0.69410956 < 0012
0.69421405 0016 >
0.69423932 0017 >
0.69443953 < 0013
0.79467082 < 0014
0.79478532 < 0015
0.79493624 0018 >
0.79496473 0019 >
0.89524573 < 0016
0.89536309 < 0017
0.89549100 0020 >
0.89552164 0021 >
0.98704302 Quitting
0.98829663 Producer quitting
0.99580252 < 0018
0.99590403 < 0019
1.09638131 < 0020
1.09647286 < 0021
1.19700873 Consumer quitting
1.19717586 Consumer quitting
ConclusionWell, that's about it. I hope I have removed some of the mystery that seems to surround Thanks for reading this article; I hope you liked it.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||