< Summary

Line coverage
52%
Covered lines: 63
Uncovered lines: 57
Coverable lines: 120
Total lines: 257
Line coverage: 52.5%
Branch coverage
38%
Covered branches: 16
Total branches: 42
Branch coverage: 38%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor()100%11100%
EnterAsync(...)66.66%6681.81%
Contended(System.Threading.CancellationToken)37.5%8853.57%
OnCancellation(System.Object,System.Threading.CancellationToken)0%880%
Exit()75%44100%
Contended()37.5%161669.69%
.ctor(...)100%11100%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.WebSockets/src/System/Net/WebSockets/AsyncMutex.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Diagnostics;
 5using System.Net;
 6using System.Threading.Tasks;
 7
 8namespace System.Threading
 9{
 10    /// <summary>Provides an async mutex.</summary>
 11    /// <remarks>
 12    /// This could be achieved with a <see cref="SemaphoreSlim"/> constructed with an initial
 13    /// and max limit of 1.  However, this implementation is optimized to the needs of ManagedWebSocket,
 14    /// which is that we expect zero contention in typical use cases.
 15    /// </remarks>
 16    internal sealed class AsyncMutex
 17    {
 18        /// <summary>Fast-path gate count tracking access to the mutex.</summary>
 19        /// <remarks>
 20        /// If the value is 1, the mutex can be entered atomically with an interlocked operation.
 21        /// If the value is less than or equal to 0, the mutex is held and requires fallback to enter it.
 22        /// </remarks>
 1728423        private int _gate = 1;
 24        /// <summary>Secondary check guarded by the lock to indicate whether the mutex is acquired.</summary>
 25        /// <remarks>
 26        /// This is only meaningful after having updated <see cref="_gate"/> via interlockeds and taken the appropriate 
 27        /// If after decrementing <see cref="_gate"/> we end up with a negative count, the mutex is contended, hence
 28        /// <see cref="_lockedSemaphoreFull"/> starting as <c>true</c>.  The primary purpose of this field
 29        /// is to handle the race condition between one thread acquiring the mutex, then another thread trying to acquir
 30        /// and getting as far as completing the interlocked operation, and then the original thread releasing; at that 
 31        /// it'll hit the lock and we need to store that the mutex is available to enter.  If we instead used a
 32        /// SemaphoreSlim as the fallback from the interlockeds, this would have been its count, and it would have start
 33        /// with an initial count of 0.
 34        /// </remarks>
 1728435        private bool _lockedSemaphoreFull = true;
 36        /// <summary>The tail of the double-linked circular waiting queue.</summary>
 37        /// <remarks>
 38        /// Waiters are added at the tail.
 39        /// Items are dequeued from the head (tail.Prev).
 40        /// </remarks>
 41        private Waiter? _waitersTail;
 42
 43        /// <summary>Gets whether the mutex is currently held by some operation (not necessarily the caller).</summary>
 44        /// <remarks>This should be used only for asserts and debugging.</remarks>
 7953045        public bool IsHeld => _gate != 1;
 46
 47        /// <summary>Gets the object used to synchronize contended operations.</summary>
 64636448        private object SyncObj => this;
 49
 50        /// <summary>Asynchronously waits to enter the mutex.</summary>
 51        /// <param name="cancellationToken">The CancellationToken token to observe.</param>
 52        /// <returns>A task that will complete when the mutex has been entered or the enter canceled.</returns>
 53        public Task EnterAsync(CancellationToken cancellationToken)
 70839254        {
 55            // If cancellation was requested, bail immediately.
 56            // If the mutex is not currently held nor contended, enter immediately.
 57            // Otherwise, fall back to a more expensive likely-asynchronous wait.
 58
 70839259            if (cancellationToken.IsCancellationRequested)
 060            {
 061                return Task.FromCanceled(cancellationToken);
 62            }
 63
 70839264            int gate = Interlocked.Decrement(ref _gate);
 70839265            if (gate >= 0)
 38521066            {
 38521067                return Task.CompletedTask;
 68            }
 69
 32318270            if (NetEventSource.Log.IsEnabled()) NetEventSource.Trace(this, $"Waiting to enter, queue length {-gate}");
 71
 32318272            return Contended(cancellationToken);
 73
 74            // Everything that follows is the equivalent of:
 75            //     return _sem.WaitAsync(cancellationToken);
 76            // if _sem were to be constructed as `new SemaphoreSlim(0)`.
 77
 78            Task Contended(CancellationToken cancellationToken)
 32318279            {
 32318280                var w = new Waiter(this);
 81
 82                // We need to register for cancellation before storing the waiter into the list.
 83                // If we registered after, we might leak a registration if the mutex was exited and the waiter
 84                // removed from the list prior to CancellationRegistration being properly assigned. By registering befor
 85                // there's a different race condition, that of cancellation being requested prior to storing the waiter 
 86                // the list; if that happens, we could end up adding the waiter and have it still stored in the list eve
 87                // though OnCancellation was called. So once we hold the lock, which OnCancellation also needs to take, 
 88                // check again whether cancellation has been requested,and avoid storing the waiter if it has.
 32318289                w.CancellationRegistration = cancellationToken.UnsafeRegister((s, token) => OnCancellation(s, token), w)
 90
 32318291                lock (SyncObj)
 32318292                {
 93                    // Now that we're holding the lock, check to see whether the async lock is acquirable.
 32318294                    if (!_lockedSemaphoreFull)
 095                    {
 96                        // If we are able to acquire the lock, we're done; we just need to clean up after the registrati
 097                        w.CancellationRegistration.Unregister();
 098                        _lockedSemaphoreFull = true;
 099                        return Task.CompletedTask;
 100                    }
 101
 102                    // Now that we're holding the lock and thus synchronized with OnCancellation, check to see
 103                    // if cancellation has been requested.
 323182104                    if (cancellationToken.IsCancellationRequested)
 0105                    {
 0106                        w.TrySetCanceled(cancellationToken);
 0107                        return w.Task;
 108                    }
 109
 110                    // The lock couldn't be acquired.
 111                    // Add the waiter to the linked list of waiters.
 323182112                    if (_waitersTail is null)
 323182113                    {
 323182114                        w.Next = w.Prev = w;
 323182115                    }
 116                    else
 0117                    {
 0118                        Debug.Assert(_waitersTail.Next != null && _waitersTail.Prev != null);
 0119                        w.Next = _waitersTail;
 0120                        w.Prev = _waitersTail.Prev;
 0121                        w.Prev.Next = w.Next.Prev = w;
 0122                    }
 323182123                    _waitersTail = w;
 323182124                }
 125
 126                // Return the waiter as a value task.
 323182127                return w.Task;
 128
 129                // Cancels the specified waiter if it's still in the list.
 130                static void OnCancellation(object? state, CancellationToken cancellationToken)
 0131                {
 0132                    Waiter? w = (Waiter)state!;
 0133                    AsyncMutex m = w.Owner;
 134
 0135                    lock (m.SyncObj)
 0136                    {
 0137                        bool inList = w.Next != null;
 0138                        if (inList)
 0139                        {
 140                            // The waiter is in the list.
 0141                            Debug.Assert(w.Prev != null);
 142
 143                            // The gate counter was decremented when this waiter was added.  We need
 144                            // to undo that.  Since the waiter is still in the list, the lock must
 145                            // still be held by someone, which means we don't need to do anything with
 146                            // the result of this increment.  If it increments to < 1, then there are
 147                            // still other waiters.  If it increments to 1, we're in a rare race condition
 148                            // where there are no other waiters and the owner just incremented the gate
 149                            // count; they would have seen it be < 1, so they will proceed to take the
 150                            // contended code path and synchronize on the lock we're holding... once we
 151                            // release it, they will appropriately update state.
 0152                            Interlocked.Increment(ref m._gate);
 153
 0154                            if (w.Next == w)
 0155                            {
 0156                                Debug.Assert(m._waitersTail == w);
 0157                                m._waitersTail = null;
 0158                            }
 159                            else
 0160                            {
 0161                                w.Next!.Prev = w.Prev;
 0162                                w.Prev.Next = w.Next;
 0163                                if (m._waitersTail == w)
 0164                                {
 0165                                    m._waitersTail = w.Next;
 0166                                }
 0167                            }
 168
 169                            // Remove it from the list.
 0170                            w.Next = w.Prev = null;
 0171                        }
 172                        else
 0173                        {
 174                            // The waiter was no longer in the list.  We must not cancel it.
 0175                            w = null;
 0176                        }
 0177                    }
 178
 179                    // If the waiter was in the list, we removed it under the lock and thus own
 180                    // the ability to cancel it.  Do so.
 0181                    w?.TrySetCanceled(cancellationToken);
 0182                }
 323182183            }
 708392184        }
 185
 186        /// <summary>Releases the mutex.</summary>
 187        /// <remarks>The caller must logically own the mutex.  This is not validated.</remarks>
 188        public void Exit()
 708392189        {
 190            // This is the equivalent of:
 191            //     _sem.Release();
 192            // if _sem were to be constructed as `new SemaphoreSlim(0)`.
 708392193            int gate = Interlocked.Increment(ref _gate);
 708392194            if (gate < 1)
 323182195            {
 323182196                if (NetEventSource.Log.IsEnabled()) NetEventSource.Trace(this, $"Unblocking next waiter on exit, remaini
 323182197                Contended();
 323182198            }
 199
 200            void Contended()
 323182201            {
 202                Waiter? w;
 323182203                lock (SyncObj)
 323182204                {
 323182205                    Debug.Assert(_lockedSemaphoreFull);
 206
 323182207                    w = _waitersTail;
 323182208                    if (w is null)
 0209                    {
 0210                        _lockedSemaphoreFull = false;
 0211                    }
 212                    else
 323182213                    {
 323182214                        Debug.Assert(w.Next != null && w.Prev != null);
 323182215                        Debug.Assert(w.Next != w || w.Prev == w);
 323182216                        Debug.Assert(w.Prev != w || w.Next == w);
 217
 323182218                        if (w.Next == w)
 323182219                        {
 323182220                            _waitersTail = null;
 323182221                        }
 222                        else
 0223                        {
 0224                            w = w.Prev; // get the head
 0225                            Debug.Assert(w.Next != null && w.Prev != null);
 0226                            Debug.Assert(w.Next != w && w.Prev != w);
 227
 0228                            w.Next.Prev = w.Prev;
 0229                            w.Prev.Next = w.Next;
 0230                        }
 231
 323182232                        w.Next = w.Prev = null;
 323182233                    }
 323182234                }
 235
 236                // Either there wasn't a waiter, or we got one and successfully removed it from the list,
 237                // at which point we own the ability to complete it.  Do so.
 323182238                if (w is not null)
 323182239                {
 323182240                    w.CancellationRegistration.Unregister();
 323182241                    w.TrySetResult();
 323182242                }
 323182243            }
 708392244        }
 245
 246        /// <summary>Represents a waiter for the mutex.</summary>
 247        private sealed class Waiter : TaskCompletionSource
 248        {
 646364249            public Waiter(AsyncMutex owner) : base(TaskCreationOptions.RunContinuationsAsynchronously) => Owner = owner;
 0250            public AsyncMutex Owner { get; }
 646364251            public CancellationTokenRegistration CancellationRegistration { get; set; }
 1939092252            public Waiter? Next { get; set; }
 1615910253            public Waiter? Prev { get; set; }
 254        }
 255    }
 256}
 257