/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Threading; using System.Threading.Tasks; namespace Opc.Ua.Client { /// /// Async-compatible reader/writer lock with writer priority. /// Many concurrent readers may hold the lock simultaneously; while /// a writer holds the lock, both readers and other writers are /// excluded. /// /// /// /// Designed as a focused, no-dependency replacement for /// Nito.AsyncEx.AsyncReaderWriterLock. The implementation /// uses a single as the writer-entry /// gate and a small synchronous critical section to atomically /// pair active-reader-count mutations with drain-signal mutations. /// /// /// Reader path (hot path): acquires the writer-entry /// semaphore briefly, increments the active-reader counter under /// lock (m_state), releases the semaphore, returns. While /// readers are active, the writer-entry semaphore is free, so /// readers do not contend with each other on it. /// /// /// Writer path (cold path): acquires and HOLDS the /// writer-entry semaphore for the full duration of the write. /// New readers therefore block on the semaphore until the writer /// releases. The writer then waits, under lock (m_state), /// for the active-reader count to reach zero — readers that were /// already in flight at the time the writer acquired the /// semaphore can drain. The drain wait is signaled by the LAST /// reader to leave (counter -> 0). Both the counter mutation and /// the drain signal are coordinated under the same /// lock (m_state), so the writer cannot observe a stale /// "drained" signal that misses a fresh reader entry. /// /// /// Cancellation: both and /// respect the supplied /// . If the writer is cancelled /// while waiting for the drain signal, the writer-entry /// semaphore is released so subsequent writers and readers can /// proceed. /// /// /// Fairness: ordering of waiters on the writer-entry /// semaphore follows .NET's /// scheduling. This is not strictly FIFO but is adequate for a /// scenario where writer entries are rare (e.g. session /// reconnect / failover) and reader entries are the common case. /// /// /// Reentrancy: not supported. A task that already holds the /// reader lock and then asks for the writer lock will deadlock /// (the writer will wait for itself to drain). Callers must avoid /// nested acquisition. /// /// internal sealed class AsyncReaderWriterLock : IDisposable { /// public void Dispose() { if (!m_isDisposed) { m_writer.Dispose(); m_isDisposed = true; } } /// /// Asynchronously acquires a reader lock. Multiple readers /// may hold the lock concurrently; a reader cannot proceed /// while a writer holds the lock. Dispose the returned /// to release. /// /// Cancellation token. public async ValueTask ReaderLockAsync( CancellationToken ct = default) { await m_writer.WaitAsync(ct).ConfigureAwait(false); try { lock (m_state) { m_activeReaders++; } } finally { m_writer.Release(); } return new Releaser(this, isWriter: false); } /// /// Asynchronously acquires the writer lock. Excludes both /// other writers and all readers for the duration the lock /// is held. Dispose the returned to /// release. /// /// Cancellation token. public async ValueTask WriterLockAsync( CancellationToken ct = default) { await m_writer.WaitAsync(ct).ConfigureAwait(false); Task? wait; lock (m_state) { if (m_activeReaders == 0) { wait = null; } else { m_drained = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); wait = m_drained.Task; } } if (wait != null) { try { await wait.WaitAsync(ct).ConfigureAwait(false); } catch { lock (m_state) { // Readers may have drained between the wait // throwing and this catch; clear the TCS // either way so the next writer creates a // fresh one. m_drained = null; } m_writer.Release(); throw; } } return new Releaser(this, isWriter: true); } /// /// Releases a reader lock. Internal — invoked by /// . /// private void ReleaseReader() { lock (m_state) { if (--m_activeReaders == 0) { // Notify a waiting writer (if any) that the last // active reader has departed. m_drained?.TrySetResult(null); m_drained = null; } } } /// /// Releases the writer lock. Internal — invoked by /// . /// private void ReleaseWriter() { m_writer.Release(); } /// /// Disposable handle returned from /// / /// . Releases the corresponding /// lock on dispose. /// public readonly struct Releaser : IDisposable { private readonly AsyncReaderWriterLock m_owner; private readonly bool m_isWriter; internal Releaser( AsyncReaderWriterLock owner, bool isWriter) { m_owner = owner; m_isWriter = isWriter; } /// public void Dispose() { if (m_owner == null) { return; } if (m_isWriter) { m_owner.ReleaseWriter(); } else { m_owner.ReleaseReader(); } } } private bool m_isDisposed; private readonly SemaphoreSlim m_writer = new(1, 1); private readonly Lock m_state = new(); private int m_activeReaders; private TaskCompletionSource? m_drained; } }