#region Copyright 2010-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0
/* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#endregion
#define SUPPORT_RECURSION
using System;
using System.Threading;
namespace CSharpTest.Net.Synchronization
{
///
/// provides a simple and fast, reader-writer lock, does not support read->write upgrades,
/// if you need an upgradeable lock, use UpgradeableReadWriteLocking
///
public class SimpleReadWriteLocking : ILockStrategy
{
/// Max number of Spin loops before polling the _event
static readonly int SpinLoops;
/// Number of iterations used for Thread.SpinWait(x)
static readonly int SpinWaitTime;
/// Setup of the SpinWaitTime/SpinLoops by processor count
static SimpleReadWriteLocking()
{ try { if (Environment.ProcessorCount > 0) { SpinWaitTime = 25; SpinLoops = 100; } } catch { SpinWaitTime = 0; SpinLoops = 0; } }
/// The event used to wake a waiting writer when a read lock is released
AutoResetEvent _event;
/// The syncronization object writers and potential readers use to lock
object _sync;
/// The total number of read locks on this lock
int _readersCount;
/// The number of readers the pending writer is waiting upon
int _targetReaders;
/// The number of time a write lock has been issued
int _writeVersion;
#if SUPPORT_RECURSION
/// The managed thread id for the thread holding the write lock
int _exclusiveThreadId;
/// The number of times the write lock thread has acquired a write lock
int _writeRecursiveCount;
#endif
///
/// Constructs the reader-writer lock using 'this' as the syncronization object
///
public SimpleReadWriteLocking()
{
_targetReaders = -1;
_sync = this;
}
///
/// Constructs the reader-writer lock using the specified object for syncronization
///
public SimpleReadWriteLocking(object syncRoot)
{
_sync = syncRoot;
}
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public virtual void Dispose()
{
object exit = Interlocked.Exchange(ref _sync, null);
if(_event != null)
_event.Close();
if (exit == null)
return;
using(new SafeLock(exit, 0))
{
if (_readersCount > 0)
throw new InvalidOperationException();
}
}
/// Changes every time a write lock is aquired. If WriteVersion == 0, no write locks have been issued.
public int WriteVersion { get { return _writeVersion; } }
///
/// Returns true if the lock was successfully obtained within the timeout specified
///
public bool TryRead(int millisecondsTimeout)
{
if (_sync == null) throw new ObjectDisposedException(GetType().FullName);
bool success = false;
try { }
finally
{
// First lock the 'writer lock' to ensure there are no writers
if (Monitor.TryEnter(_sync, millisecondsTimeout))
{
// Safe to increment the read counter since there are no writers
Interlocked.Increment(ref _readersCount);
// Release the lock, we are done
Monitor.Exit(_sync);
success = true;
}
}
return success;
}
///
/// Releases a read lock
///
public void ReleaseRead()
{
if (_sync == null) throw new ObjectDisposedException(GetType().FullName);
try { }
finally
{
// Decrement the reader count and, if we are the last, set the wake event for a writer
int newCount = Interlocked.Decrement(ref _readersCount);
if (_targetReaders == newCount && _event != null)
_event.Set();
if (newCount < 0)
{
Interlocked.Increment(ref newCount);
throw new SynchronizationLockException();
}
}
}
///
/// Returns true if the lock was successfully obtained within the timeout specified
///
public virtual bool TryWrite(int millisecondsTimeout)
{
bool success = false;
try { }
finally
{
if (_sync == null) throw new ObjectDisposedException(GetType().FullName);
// First obtain the 'writer lock':
if (Monitor.TryEnter(_sync, millisecondsTimeout))
{
// Now that we have the lock, wait for the readers to release
if (WaitForExclusive(0, millisecondsTimeout))
success = true;
else
Monitor.Exit(_sync);
}
}
return success;
}
///
/// This is the only real work to be done, once we've acquired the write lock
/// we have to wait for all readers to complete. If/when that happens we can
/// then own the write lock. The case where this does not take place is when
/// a thread that already owns the lock calls us to lock again. In this case
/// we can just return success and ignore the outstanding read requests. The
/// major problem with this approach is that if function A() does a read-lock
/// and calls function B() which does a write lock, this will fail. So the
/// solution is to either use the upgradeable version (see the derived class
/// UpgradableReadWriteLocking) and upgrade, or to start with a write lock in
/// function A().
///
protected bool WaitForExclusive(int targetReaders, int millisecondsTimeout)
{
// if this thread is already the writer, we can just return
#if SUPPORT_RECURSION
int threadId = Thread.CurrentThread.ManagedThreadId;
if (_exclusiveThreadId == threadId)
{
_writeRecursiveCount++;
return true;
}
#endif
// convert the timeout to a positive number
if (millisecondsTimeout < 0)
millisecondsTimeout = int.MaxValue;
// set the number of readers we are looking for so that when that number
// is reached by ReleaseRead it will signal us. Obviously the event and
// several things about it are not syncronized and as such require the
// polling loop we are about to start...
if (_event == null && _readersCount > targetReaders)
_event = new AutoResetEvent(false);
_targetReaders = targetReaders;
// If SpinWaitTime is zero we are on a single-proc host and don't want to
// consume the CPU, otherwise we want to start polling by using a tight
// spin loop waiting for the readers to release. You might notice that
// our timeout is anything but accurate here, but it should prove to be
// 'ballpark' close :)
int loop = 0;
int spinLoops = Math.Min(millisecondsTimeout, SpinLoops);
while (_readersCount > targetReaders && loop < millisecondsTimeout)
{
// first few rounds we use a spin loop
if (loop < spinLoops)
{
loop++;
Thread.SpinWait(SpinWaitTime);
continue;
}
// if we are still waiting on readers after spinning for a few loops
// we can just start polling the event.
if (!_event.WaitOne(10, false))
loop += 10;
}
// clear the expected reader count, so sense in reader threads setting the
// event over and over again.
_targetReaders = -1;
// if we failed to hit our target reader count we just leave.
if (_readersCount > targetReaders)
return false;
// set the number of writers, increment the version and we are done
_writeVersion++;
#if SUPPORT_RECURSION
_writeRecursiveCount = 1;
_exclusiveThreadId = threadId;
#endif
return true;
}
///
/// Releases a writer lock
///
public void ReleaseWrite()
{
if (_sync == null) throw new ObjectDisposedException(GetType().FullName);
try { }
finally
{
#if SUPPORT_RECURSION
// if we are the exclusive thread, and the last ReleaseWrite that will be called, we can then
// clear the captured thread id for the exclusive writer.
if (_exclusiveThreadId == Thread.CurrentThread.ManagedThreadId && --_writeRecursiveCount == 0)
_exclusiveThreadId = 0;
#endif
// Now just release the lock once (btw, this thread may still have locks on this)
Monitor.Exit(_sync);
}
}
///
/// Returns a reader lock that can be elevated to a write lock
///
public ReadLock Read() { return ReadLock.Acquire(this, -1); }
///
/// Returns a reader lock that can be elevated to a write lock
///
///
public ReadLock Read(int millisecondsTimeout) { return ReadLock.Acquire(this, millisecondsTimeout); }
///
/// Returns a read and write lock
///
public WriteLock Write() { return WriteLock.Acquire(this, -1); }
///
/// Returns a read and write lock
///
///
public WriteLock Write(int millisecondsTimeout) { return WriteLock.Acquire(this, millisecondsTimeout); }
}
}