N
not_a_commie
I put together the code below for some training at work. It contains
numerous methods for doing thread synchronization. Compile it in
release mode to see the various speeds. Change the functions in the
two "for" loops at the bottom (but keep them both the same function).
You'll need a multi-core CPU to see any problem with the non-protected
increment as the CLR is not dumb enough to break up an increment
function when scheduling threads on the same core. I never did figure
out how the Monitor class itself does locking. It may be using some
kind of dictionary / data structure that doesn't require locking for
the TryGetValue call. Have any of you seen a data collection structure
with a "TryGetValueOrAddNew" method that is natively thread safe?
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
namespace DemoInc
{
class Program
{
#region Locker
struct Locker : IDisposable
{
readonly object _object;
public Locker(object obj)
{
Monitor.Enter(obj);
_object = obj;
// optional: log something here or add a timeout constructor
}
public void Dispose()
{
Monitor.Exit(_object);
}
}
#endregion
#region Bran's Sempahore and Mutex
public class BransSemaphore : IDisposable
{
protected volatile int _currentCount;
protected readonly int _maxCount;
protected readonly ManualResetEvent _acquire = new ManualResetEvent
(false); // these things are darn slow (they're made to be system-wide
objects)
protected readonly ManualResetEvent _release = new ManualResetEvent
(false);
protected readonly object _lock = new object();
public BransSemaphore(int currentCount, int maxCount)
{
if (currentCount < 0)
throw new ArgumentOutOfRangeException("currentCount");
if (maxCount < 1)
throw new ArgumentOutOfRangeException("maxCount");
_currentCount = currentCount;
_maxCount = maxCount;
}
public void Acquire()
{
do
{
lock (_lock)
{
_release.Reset();
if (_currentCount > 0)
{
_currentCount--;
_acquire.Set();
return;
}
}
_release.WaitOne();
} while (true);
}
public void Release()
{
do
{
lock (_lock)
{
_acquire.Reset();
if (_currentCount < _maxCount)
{
_currentCount++;
_release.Set();
return;
}
_acquire.WaitOne();
}
} while (true);
}
public void Dispose()
{
_acquire.Close();
_release.Close();
}
}
public class BransMutex : BransSemaphore
{
public BransMutex() : base(1, 1) { }
}
public class BransSpinSemaphore
{
protected volatile int _currentCount;
protected readonly int _maxCount;
protected readonly object _lock = new object();
public BransSpinSemaphore(int currentCount, int maxCount)
{
if (currentCount < 0)
throw new ArgumentOutOfRangeException("currentCount");
if (maxCount < 1)
throw new ArgumentOutOfRangeException("maxCount");
_currentCount = currentCount;
_maxCount = maxCount;
}
public void Acquire()
{
do
{
lock (_lock)
{
if (_currentCount > 0)
{
_currentCount--;
return;
}
}
Thread.Sleep(0);
} while (true);
}
public void Release()
{
do
{
lock (_lock)
{
if (_currentCount < _maxCount)
{
_currentCount++;
return;
}
}
Thread.Sleep(0);
} while (true);
}
}
public class BransSpinMutex : BransSpinSemaphore
{
public BransSpinMutex() : base(1, 1) { }
}
#endregion
#region Bran's Monitor
static class BransMonitor
{
private static readonly object _lock = new object();
private static readonly Dictionary<object, int> _currentRunner =
new Dictionary<object, int>();
public static void Enter(object obj)
{
//look for obj in Dict
//if there compare threadId
// if equal or missing return
//else wait for missing
int threadId = Thread.CurrentThread.ManagedThreadId;
do
{
lock (_lock) // I have no idea how the built-in Monitor class
does their locking -- it must be specially built into the CLR
{
int current;
if (_currentRunner.TryGetValue(obj, out current))
{
if (current == threadId)
return; // it's the same thread; let them go
}
else
{
// nobody is in there
_currentRunner.Add(obj, threadId);
return;
}
// we need to wait because a different thread is running
}
Thread.Sleep(0); // pseudo spin lock
} while (true);
}
public static void Exit(object obj)
{
int threadId = Thread.CurrentThread.ManagedThreadId;
do
{
lock (_lock) // I have no idea how the built-in Monitor class
does their locking -- it must be specially built into the CLR
{
int current;
if (_currentRunner.TryGetValue(obj, out current))
{
if (current == threadId)
{
_currentRunner.Remove(obj);
return; // it's the same thread; release it
}
}
else
{
// nobody is in there
// ignore it
return;
}
// we need to wait because a different thread has the lock
}
Thread.Sleep(0); // pseudo spin lock
} while (true);
}
}
#endregion
#region Increment Demonstration Class
class Incer : IDisposable
{
private int _count;
public void Inc() // completely unsafe
{
_count++;
}
private readonly object _lock = new object(); // static or not?
public void IncLock() // the traditional method
{
lock (_lock)
_count++;
}
public void IncMonitor() // equivalent to the lock keyword
{
Monitor.Enter(_lock);
try
{
_count++;
}
finally
{
Monitor.Exit(_lock);
}
}
public void IncLocker() // same as the lock keyword; this just
shows how it works
{
using (new Locker(_lock)) // this does allocate one additional
thing on the stack (it's a struct)
_count++;
}
public void IncInterlocked()
{
Interlocked.Increment(ref _count); // the preferred method
wherever possible
}
private int _exch = 1; // 1 means allowed in
public void IncExchange() // this demos a spin lock if we change
the sleep to be a NOP
{
while (Interlocked.Exchange(ref _exch, 0) <= 0) Thread.Sleep(0);
_count++;
Interlocked.Exchange(ref _exch, 1);
}
private readonly Mutex _mutex = new Mutex(); // same as Semaphore
(1,1) and incredibly slow
public void IncMutex()
{
_mutex.WaitOne(); // this, as in all things being disposed in the
finally clause, should be outside the try
try
{
_count++;
}
finally // no performance affect
{
_mutex.ReleaseMutex();
}
}
private readonly Semaphore _sem = new Semaphore(1, 1); // really
slow
public void IncSemaphore()
{
_sem.WaitOne();
try
{
_count++;
}
finally
{
_sem.Release();
}
}
private readonly BransMutex _bm = new BransMutex(); // same as
Semaphore(1,1)
public void IncBransMutex()
{
_bm.Acquire(); // this, as in all things being disposed in the
finally clause, should be outside the try
try
{
_count++;
}
finally // no performance affect
{
_bm.Release();
}
}
private readonly BransSemaphore _bs = new BransSemaphore(1, 1); //
system wide semaphore; she's expensive
public void IncBransSemaphore()
{
_bs.Acquire();
try
{
_count++;
}
finally
{
_bs.Release();
}
}
private readonly BransSpinMutex _bsm = new BransSpinMutex(); //
same as Semaphore(1,1)
public void IncBransSpinMutex()
{
_bsm.Acquire(); // this, as in all things being disposed in the
finally clause, should be outside the try
try
{
_count++;
}
finally // no performance affect
{
_bsm.Release();
}
}
private readonly BransSpinSemaphore _bss = new BransSpinSemaphore
(1, 1); // system wide semaphore; she's expensive
public void IncBransSpinSemaphore()
{
_bss.Acquire();
try
{
_count++;
}
finally
{
_bss.Release();
}
}
public void IncBransMonitor()
{
BransMonitor.Enter(_lock);
try
{
_count++;
}
finally // no performance affect
{
BransMonitor.Exit(_lock);
}
}
public int Count { get { return _count; } }
public void Dispose()
{
_mutex.Close();
_sem.Close();
_bm.Dispose();
_bs.Dispose();
}
}
#endregion
#region Main Method
[STAThread]
static void Main(string[] args)
{
var incer = new Incer();
var sw1 = new Stopwatch();
var sw2 = new Stopwatch();
var thread1 = new Thread(() =>
{
sw1.Start(); for (int i = 0; i < 5000000; i++) incer.IncInterlocked
(); sw1.Stop();
});
var thread2 = new Thread(() =>
{
sw2.Start(); for (int i = 0; i < 5000000; i++) incer.IncInterlocked
(); sw2.Stop();
});
thread1.Name = "t1";
thread1.SetApartmentState(ApartmentState.STA);
thread2.Name = "t2";
thread2.SetApartmentState(ApartmentState.STA);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.Out.WriteLine("Counted to {0} in {1} sec.", incer.Count,
sw1.Elapsed.TotalSeconds + sw2.Elapsed.TotalSeconds);
incer.Dispose();
}
#endregion
}
}
numerous methods for doing thread synchronization. Compile it in
release mode to see the various speeds. Change the functions in the
two "for" loops at the bottom (but keep them both the same function).
You'll need a multi-core CPU to see any problem with the non-protected
increment as the CLR is not dumb enough to break up an increment
function when scheduling threads on the same core. I never did figure
out how the Monitor class itself does locking. It may be using some
kind of dictionary / data structure that doesn't require locking for
the TryGetValue call. Have any of you seen a data collection structure
with a "TryGetValueOrAddNew" method that is natively thread safe?
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
namespace DemoInc
{
class Program
{
#region Locker
struct Locker : IDisposable
{
readonly object _object;
public Locker(object obj)
{
Monitor.Enter(obj);
_object = obj;
// optional: log something here or add a timeout constructor
}
public void Dispose()
{
Monitor.Exit(_object);
}
}
#endregion
#region Bran's Sempahore and Mutex
public class BransSemaphore : IDisposable
{
protected volatile int _currentCount;
protected readonly int _maxCount;
protected readonly ManualResetEvent _acquire = new ManualResetEvent
(false); // these things are darn slow (they're made to be system-wide
objects)
protected readonly ManualResetEvent _release = new ManualResetEvent
(false);
protected readonly object _lock = new object();
public BransSemaphore(int currentCount, int maxCount)
{
if (currentCount < 0)
throw new ArgumentOutOfRangeException("currentCount");
if (maxCount < 1)
throw new ArgumentOutOfRangeException("maxCount");
_currentCount = currentCount;
_maxCount = maxCount;
}
public void Acquire()
{
do
{
lock (_lock)
{
_release.Reset();
if (_currentCount > 0)
{
_currentCount--;
_acquire.Set();
return;
}
}
_release.WaitOne();
} while (true);
}
public void Release()
{
do
{
lock (_lock)
{
_acquire.Reset();
if (_currentCount < _maxCount)
{
_currentCount++;
_release.Set();
return;
}
_acquire.WaitOne();
}
} while (true);
}
public void Dispose()
{
_acquire.Close();
_release.Close();
}
}
public class BransMutex : BransSemaphore
{
public BransMutex() : base(1, 1) { }
}
public class BransSpinSemaphore
{
protected volatile int _currentCount;
protected readonly int _maxCount;
protected readonly object _lock = new object();
public BransSpinSemaphore(int currentCount, int maxCount)
{
if (currentCount < 0)
throw new ArgumentOutOfRangeException("currentCount");
if (maxCount < 1)
throw new ArgumentOutOfRangeException("maxCount");
_currentCount = currentCount;
_maxCount = maxCount;
}
public void Acquire()
{
do
{
lock (_lock)
{
if (_currentCount > 0)
{
_currentCount--;
return;
}
}
Thread.Sleep(0);
} while (true);
}
public void Release()
{
do
{
lock (_lock)
{
if (_currentCount < _maxCount)
{
_currentCount++;
return;
}
}
Thread.Sleep(0);
} while (true);
}
}
public class BransSpinMutex : BransSpinSemaphore
{
public BransSpinMutex() : base(1, 1) { }
}
#endregion
#region Bran's Monitor
static class BransMonitor
{
private static readonly object _lock = new object();
private static readonly Dictionary<object, int> _currentRunner =
new Dictionary<object, int>();
public static void Enter(object obj)
{
//look for obj in Dict
//if there compare threadId
// if equal or missing return
//else wait for missing
int threadId = Thread.CurrentThread.ManagedThreadId;
do
{
lock (_lock) // I have no idea how the built-in Monitor class
does their locking -- it must be specially built into the CLR
{
int current;
if (_currentRunner.TryGetValue(obj, out current))
{
if (current == threadId)
return; // it's the same thread; let them go
}
else
{
// nobody is in there
_currentRunner.Add(obj, threadId);
return;
}
// we need to wait because a different thread is running
}
Thread.Sleep(0); // pseudo spin lock
} while (true);
}
public static void Exit(object obj)
{
int threadId = Thread.CurrentThread.ManagedThreadId;
do
{
lock (_lock) // I have no idea how the built-in Monitor class
does their locking -- it must be specially built into the CLR
{
int current;
if (_currentRunner.TryGetValue(obj, out current))
{
if (current == threadId)
{
_currentRunner.Remove(obj);
return; // it's the same thread; release it
}
}
else
{
// nobody is in there
// ignore it
return;
}
// we need to wait because a different thread has the lock
}
Thread.Sleep(0); // pseudo spin lock
} while (true);
}
}
#endregion
#region Increment Demonstration Class
class Incer : IDisposable
{
private int _count;
public void Inc() // completely unsafe
{
_count++;
}
private readonly object _lock = new object(); // static or not?
public void IncLock() // the traditional method
{
lock (_lock)
_count++;
}
public void IncMonitor() // equivalent to the lock keyword
{
Monitor.Enter(_lock);
try
{
_count++;
}
finally
{
Monitor.Exit(_lock);
}
}
public void IncLocker() // same as the lock keyword; this just
shows how it works
{
using (new Locker(_lock)) // this does allocate one additional
thing on the stack (it's a struct)
_count++;
}
public void IncInterlocked()
{
Interlocked.Increment(ref _count); // the preferred method
wherever possible
}
private int _exch = 1; // 1 means allowed in
public void IncExchange() // this demos a spin lock if we change
the sleep to be a NOP
{
while (Interlocked.Exchange(ref _exch, 0) <= 0) Thread.Sleep(0);
_count++;
Interlocked.Exchange(ref _exch, 1);
}
private readonly Mutex _mutex = new Mutex(); // same as Semaphore
(1,1) and incredibly slow
public void IncMutex()
{
_mutex.WaitOne(); // this, as in all things being disposed in the
finally clause, should be outside the try
try
{
_count++;
}
finally // no performance affect
{
_mutex.ReleaseMutex();
}
}
private readonly Semaphore _sem = new Semaphore(1, 1); // really
slow
public void IncSemaphore()
{
_sem.WaitOne();
try
{
_count++;
}
finally
{
_sem.Release();
}
}
private readonly BransMutex _bm = new BransMutex(); // same as
Semaphore(1,1)
public void IncBransMutex()
{
_bm.Acquire(); // this, as in all things being disposed in the
finally clause, should be outside the try
try
{
_count++;
}
finally // no performance affect
{
_bm.Release();
}
}
private readonly BransSemaphore _bs = new BransSemaphore(1, 1); //
system wide semaphore; she's expensive
public void IncBransSemaphore()
{
_bs.Acquire();
try
{
_count++;
}
finally
{
_bs.Release();
}
}
private readonly BransSpinMutex _bsm = new BransSpinMutex(); //
same as Semaphore(1,1)
public void IncBransSpinMutex()
{
_bsm.Acquire(); // this, as in all things being disposed in the
finally clause, should be outside the try
try
{
_count++;
}
finally // no performance affect
{
_bsm.Release();
}
}
private readonly BransSpinSemaphore _bss = new BransSpinSemaphore
(1, 1); // system wide semaphore; she's expensive
public void IncBransSpinSemaphore()
{
_bss.Acquire();
try
{
_count++;
}
finally
{
_bss.Release();
}
}
public void IncBransMonitor()
{
BransMonitor.Enter(_lock);
try
{
_count++;
}
finally // no performance affect
{
BransMonitor.Exit(_lock);
}
}
public int Count { get { return _count; } }
public void Dispose()
{
_mutex.Close();
_sem.Close();
_bm.Dispose();
_bs.Dispose();
}
}
#endregion
#region Main Method
[STAThread]
static void Main(string[] args)
{
var incer = new Incer();
var sw1 = new Stopwatch();
var sw2 = new Stopwatch();
var thread1 = new Thread(() =>
{
sw1.Start(); for (int i = 0; i < 5000000; i++) incer.IncInterlocked
(); sw1.Stop();
});
var thread2 = new Thread(() =>
{
sw2.Start(); for (int i = 0; i < 5000000; i++) incer.IncInterlocked
(); sw2.Stop();
});
thread1.Name = "t1";
thread1.SetApartmentState(ApartmentState.STA);
thread2.Name = "t2";
thread2.SetApartmentState(ApartmentState.STA);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.Out.WriteLine("Counted to {0} in {1} sec.", incer.Count,
sw1.Elapsed.TotalSeconds + sw2.Elapsed.TotalSeconds);
incer.Dispose();
}
#endregion
}
}