/* * Copyright (c) 2024, Sjofn LLC * All rights reserved. * * - Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Neither the name of the openmetaverse.co nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Threading; using LibreMetaverse.Threading.Disposers; using LockIntegralType = System.Int32; // I am using direct reads that are non-atomic if 64-bits variables are used on 32-bit computers. namespace LibreMetaverse.Threading { /// /// An optimistic reader writer lock that is as fast as the /// SpinReaderWriterLock/SpinReaderWriterLockSlim when the lock /// can be obtained immediately. But, if that's not the case, instead /// of spinning it will enter a real wait state. So, this one is preferable /// if the locks are expected to be of large duration (100 milliseconds or more) /// while the SpinReaderWriterLock is preferable if the waits are usually /// very small. /// Note that this class has both the Enter/Exit pairs (slim version) and the /// methods that return a disposable object to release the lock (the non-slim /// version). /// public sealed class OptimisticReaderWriterLock: IReaderWriterLockSlim, IReaderWriterLock { #region Consts private const LockIntegralType WRITE_BIT_SHIFT = 24; private const LockIntegralType UPGRADE_BIT_SHIFT = 16; private const LockIntegralType WRITE_LOCK_VALUE = 1 << WRITE_BIT_SHIFT; private const LockIntegralType WRITE_UNLOCK_VALUE = -WRITE_LOCK_VALUE; private const LockIntegralType UPGRADE_LOCK_VALUE = 1 << UPGRADE_BIT_SHIFT; private const LockIntegralType UPGRADE_UNLOCK_VALUE = -UPGRADE_LOCK_VALUE; private const LockIntegralType ALL_READS_VALUE = UPGRADE_LOCK_VALUE-1; private const LockIntegralType SOME_EXCLUSIVE_LOCK_VALUE = WRITE_LOCK_VALUE | UPGRADE_LOCK_VALUE; private const LockIntegralType SOME_EXCLUSIVE_UNLOCK_VALUE = -SOME_EXCLUSIVE_LOCK_VALUE; #endregion #region Fields private LockIntegralType _lockValue; private LockIntegralType _waitingValue; #endregion #region ReadLock /// /// Acquires a read lock that must be used in a using clause. /// public OptimisticReadLock ReadLock() { EnterReadLock(); return new OptimisticReadLock(this); } #endregion #region UpgradeableLock /// /// Acquires a upgradeable read lock that must be used in a using clause. /// public OptimisticUpgradeableLock UpgradeableLock() { EnterUpgradeableLock(); return new OptimisticUpgradeableLock(this); } #endregion #region WriteLock /// /// Acquires a write lock that must be used in a using clause. /// If you are using a UpgradeableLock use the Upgrade method of the /// YieldUpgradeableLock instead or you will cause a dead-lock. /// public OptimisticWriteLock WriteLock() { EnterWriteLock(); return new OptimisticWriteLock(this); } #endregion #region EnterReadLock /// /// Enters a read lock. /// public void EnterReadLock() { while(true) { LockIntegralType result = Interlocked.Increment(ref _lockValue); if (result < WRITE_LOCK_VALUE) return; lock(this) { // here we can read directly. // also, if everything is OK we can return // directly as we still hold the readers count // at +1. if (_lockValue < WRITE_LOCK_VALUE) return; _waitingValue++; result = Interlocked.Decrement(ref _lockValue); if (result < WRITE_LOCK_VALUE) { _waitingValue--; continue; } while(true) { Monitor.Wait(this); // again, we can read directly. if (_lockValue < WRITE_LOCK_VALUE) { _waitingValue--; break; } } } } } #endregion #region ExitReadLock /// /// Exits a read-lock. Take care not to exit more times than you entered, as there is no check for that. /// public void ExitReadLock() { int result = Interlocked.Decrement(ref _lockValue); if ((result & ALL_READS_VALUE) == 0) if (_waitingValue > 0) lock(this) Monitor.PulseAll(this); // We need to Pulse all threads when there are no more readers // because we may have a thread waiting to obtain a write lock and an // upgradeable lock trying to upgrade. A simple pulse will free the // thread that wants the write lock, but it will not be able to get // it because there's the upgradeable lock already acquired. } #endregion #region EnterUpgradeableLock /// /// Enters an upgradeable lock (it is a read lock, but it can be upgraded). /// Only one upgradeable lock is allowed at a time. /// public void EnterUpgradeableLock() { while(true) { LockIntegralType result = Interlocked.Add(ref _lockValue, UPGRADE_LOCK_VALUE); if ((result >> UPGRADE_BIT_SHIFT) == 1) return; lock(this) { // here we can read directly. // also, if everything is OK we can return // directly as we still hold the upgradeable count // at +1. if ((_lockValue >> UPGRADE_BIT_SHIFT) == 1) return; _waitingValue ++; result = Interlocked.Add(ref _lockValue, UPGRADE_UNLOCK_VALUE); if ((result >> UPGRADE_BIT_SHIFT) == 0) { _waitingValue --; continue; } // maybe we just forbid the thread that has the // upgradeable lock from upgrading to the write lock. // Also, as this may not be the next thread waiting, we // need to pulse all. if ((result >> UPGRADE_BIT_SHIFT) == 1) Monitor.PulseAll(this); while(true) { Monitor.Wait(this); // again, we can read directly. if ((_lockValue >> UPGRADE_BIT_SHIFT) == 0) { _waitingValue --; break; } } } } } #endregion #region ExitUpgradeableLock /// /// Exits a previously obtained upgradeable lock without /// verifying if it was upgraded or not. /// public void UncheckedExitUpgradeableLock() { var result = Interlocked.Add(ref _lockValue, UPGRADE_UNLOCK_VALUE); if ((result & ALL_READS_VALUE) == 0) if (_waitingValue > 0) lock(this) Monitor.Pulse(this); // Here we pulse, instead of PulseAll, because we never block // readers and it is guaranteed that there was no other thread with // an upgradeable lock trying to upgrade it. So, if there's a thread // trying to obtain either an upgradeable or writeable lock, we pulse // that single thread. } /// /// Exits a previously entered upgradeable lock. /// public void ExitUpgradeableLock(bool upgraded) { if (upgraded) UncheckedExitUpgradedLock(); else UncheckedExitUpgradeableLock(); } #endregion #region UpgradeToWriteLock /// /// Upgrades to write-lock. You must already own a Upgradeable lock and you must first exit the write lock then the upgradeable lock. /// public void UncheckedUpgradeToWriteLock() { while(true) { LockIntegralType result = Interlocked.CompareExchange(ref _lockValue, SOME_EXCLUSIVE_LOCK_VALUE, UPGRADE_LOCK_VALUE); if (result == UPGRADE_LOCK_VALUE) return; lock(this) { if (_lockValue == UPGRADE_LOCK_VALUE) continue; _waitingValue ++; Monitor.Wait(this); while(true) { if (_lockValue == UPGRADE_LOCK_VALUE) { _waitingValue --; break; } Monitor.Wait(this); } } } } /// /// upgrades to write-lock. You must already own a Upgradeable lock and you must first exit the write lock then the Upgradeable lock. /// public void UpgradeToWriteLock(ref bool upgraded) { if (upgraded) return; UncheckedUpgradeToWriteLock(); upgraded = true; } #endregion #region UncheckedExitUpgradedLock /// /// Releases the Upgradeable lock and the upgraded version of it (the write lock) /// at the same time. /// Releasing the write lock and the upgradeable lock has the same effect, but /// it's slower. /// public void UncheckedExitUpgradedLock() { Interlocked.Add(ref _lockValue, SOME_EXCLUSIVE_UNLOCK_VALUE); if (_waitingValue > 0) lock(this) Monitor.PulseAll(this); } #endregion #region EnterWriteLock /// /// Enters write-lock. /// public void EnterWriteLock() { while(true) { LockIntegralType result = Interlocked.CompareExchange(ref _lockValue, WRITE_LOCK_VALUE, 0); if (result == 0) return; lock(this) { if (_lockValue == 0) continue; _waitingValue ++; Monitor.Wait(this); while(true) { if (_lockValue == 0) { _waitingValue --; break; } Monitor.Wait(this); } } } } #endregion #region ExitWriteLock /// /// Exits write lock. Take care to exit only when you entered, as there is no check for that. /// public void ExitWriteLock() { Interlocked.Add(ref _lockValue, WRITE_UNLOCK_VALUE); if (_waitingValue > 0) lock(this) Monitor.PulseAll(this); } #endregion #region IReaderWriterLock Private Implementation IDisposable IReaderWriterLock.ReadLock() { return ReadLock(); } IUpgradeableLock IReaderWriterLock.UpgradeableLock() { return UpgradeableLock(); } IDisposable IReaderWriterLock.WriteLock() { return WriteLock(); } #endregion } }