I'm doing a very silly benchmark on the ReaderWriterLock with this code, where reading happens 4x more often than writting:
class Program
{
static void Main()
{
ISynchro[] test = { new Locked(), new RWLocked() };
Stopwatch sw = new Stopwatch();
foreach ( var isynchro in test )
{
sw.Reset();
sw.Start();
Thread w1 = new Thread( new ParameterizedThreadStart( WriteThread ) );
w1.Start( isynchro );
Thread w2 = new Thread( new ParameterizedThreadStart( WriteThread ) );
w2.Start( isynchro );
Thread r1 = new Thread( new ParameterizedThreadStart( ReadThread ) );
r1.Start( isynchro );
Thread r2 = new Thread( new ParameterizedThreadStart( ReadThread ) );
r2.Start( isynchro );
w1.Join();
w2.Join();
r1.Join();
r2.Join();
sw.Stop();
Console.WriteLine( isynchro.ToString() + ": " + sw.ElapsedMilliseconds.ToString() + "ms." );
}
Console.WriteLine( "End" );
Console.ReadKey( true );
}
static void ReadThread(Object o)
{
ISynchro synchro = (ISynchro)o;
for ( int i = 0; i < 500; i++ )
{
Int32? value = synchro.Get( i );
Thread.Sleep( 50 );
}
}
static void WriteThread( Object o )
{
ISynchro synchro = (ISynchro)o;
for ( int i = 0; i < 125; i++ )
{
synchro.Add( i );
Thread.Sleep( 200 );
}
}
}
interface ISynchro
{
void Add( Int32 value );
Int32? Get( Int32 index );
}
class Locked:List<Int32>, ISynchro
{
readonly Object locker = new object();
#region ISynchro Members
public new void Add( int value )
{
lock ( locker )
base.Add( value );
}
public int? Get( int index )
{
lock ( locker )
{
if ( this.Count <= index )
return null;
return this[ index ];
}
}
#endregion
public override string ToString()
{
return "Locked";
}
}
class RWLocked : List<Int32>, ISynchro
{
ReaderWriterLockSlim locker = new ReaderWriterLockSlim();
#region ISynchro Members
public new void Add( int value )
{
try
{
locker.EnterWriteLock();
base.Add( value );
}
finally
{
locker.ExitWriteLock();
}
}
public int? Get( int index )
{
try
{
locker.EnterReadLock();
if ( this.Count <= index )
return null;
return this[ index ];
}
finally
{
locker.ExitReadLock();
}
}
#endregion
public override string ToString()
{
return "RW Locked";
}
}
But I get that both perform in more or less the same way:
Locked: 25003ms.
RW Locked: 25002ms.
End
Even making the read 20 times more often that writes, the performance is still (almost) the same.
Am I doing something wrong here?
Kind regards.
When you have significantly more reads than writes.
In your example, the sleeps mean that generally there is no contention. An uncontended lock is very fast. For this to matter, you would need a contended lock; if there are writes in that contention, they should be about the same (
lock
may even be quicker) - but if it is mostly reads (with a write contention rarely), I would expect theReaderWriterLockSlim
lock to out-perform thelock
.Personally, I prefer another strategy here, using reference-swapping - so reads can always read without ever checking / locking / etc. Writes make their change to a cloned copy, then use
Interlocked.CompareExchange
to swap the reference (re-applying their change if another thread mutated the reference in the interim).There's no contention in this program. The Get and Add methods execute in a few nanoseconds. The odds that multiple threads hit those methods at the exact time are vanishingly small.
Put a Thread.Sleep(1) call in them and remove the sleep from the threads to see the difference.
Uncontested locks take on the order of microseconds to acquire, so your execution time will be dwarfed by your calls to
Sleep
.You will get better performance with
ReaderWriterLockSlim
than a simple lock if you lock a part of code which needs longer time to execute. In this case readers can work in parallel. Acquiring aReaderWriterLockSlim
takes more time than entering a simpleMonitor
. Check myReaderWriterLockTiny
implementation for a readers-writer lock which is even faster than simple lock statement and offers a readers-writer functionality: http://i255.wordpress.com/2013/10/05/fast-readerwriterlock-for-net/Edit 2: Simply removing the
Thread.Sleep
calls fromReadThread
andWriteThread
, I sawLocked
outperformRWLocked
. I believe Hans hit the nail on the head here; your methods are too fast and create no contention. When I addedThread.Sleep(1)
to theGet
andAdd
methods ofLocked
andRWLocked
(and used 4 read threads against 1 write thread),RWLocked
beat the pants off ofLocked
.Edit: OK, if I were actually thinking when I first posted this answer, I would've realized at least why you put the
Thread.Sleep
calls in there: you were trying to reproduce the scenario of reads happening more frequently than writes. This is just not the right way to do that. Instead, I would introduce extra overhead to yourAdd
andGet
methods to create a greater chance of contention (as Hans suggested), create more read threads than write threads (to ensure more frequent reads than writes), and remove theThread.Sleep
calls fromReadThread
andWriteThread
(which actually reduce contention, achieving the opposite of what you want).I like what you've done so far. But here are a few issues I see right off the bat:
Thread.Sleep
calls? These are just inflating your execution times by a constant amount, which is going to artificially make performance results converge.Thread
objects in the code that's measured by yourStopwatch
. That is not a trivial object to create.Whether you will see a significant difference once you address the two issues above, I don't know. But I believe they should be addressed before the discussion continues.