Skip to content

Commit 0eabff5

Browse files
committed
Add AsyncMutex class
1 parent fcfe71f commit 0eabff5

File tree

6 files changed

+448
-0
lines changed

6 files changed

+448
-0
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.VisualStudio.Threading;
10+
11+
/// <summary>
12+
/// A mutex that can be entered asynchronously.
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// This mutex utilizes an OS synchronization primitive which is fundamentally thread-affinitized and requires synchronously blocking the thread that will own the mutex.
17+
/// This makes a native mutex unsuitable for use in async methods, where the thread that enters the mutex may not be the same thread that exits it.
18+
/// This class solves that problem by using a private dedicated thread to enter and release the mutex, but otherwise allows its owner to execute async code, switch threads, etc.
19+
/// </para>
20+
/// </remarks>
21+
public class AsyncMutex : IDisposable
22+
{
23+
#pragma warning disable SA1310 // Field names should not contain underscore
24+
private const int STATE_READY = 0;
25+
private const int STATE_HELD_OR_WAITING = 1;
26+
private const int STATE_DISPOSED = 2;
27+
#pragma warning restore SA1310 // Field names should not contain underscore
28+
29+
private static readonly Action ExitSentinel = new Action(() => { });
30+
private readonly Thread namedMutexOwner;
31+
private readonly BlockingCollection<Action> mutexWorkQueue = new();
32+
private readonly Mutex mutex;
33+
private int state;
34+
35+
/// <summary>
36+
/// Initializes a new instance of the <see cref="AsyncMutex"/> class.
37+
/// </summary>
38+
/// <param name="name"><inheritdoc cref="Mutex(bool, string)" path="/param[@name='name']"/></param>
39+
/// <remarks>
40+
/// See the help docs on the underlying <see cref="Mutex"/> class for more information on the <paramref name="name"/> parameter.
41+
/// Consider when reading that the <c>initiallyOwned</c> parameter for that constructor is always <see langword="false"/> for this class.
42+
/// </remarks>
43+
public AsyncMutex(string? name)
44+
{
45+
this.namedMutexOwner = new Thread(this.MutexOwnerThread, 10 * 1024)
46+
{
47+
Name = $"{nameof(AsyncMutex)}-{name}",
48+
};
49+
this.mutex = new Mutex(false, name);
50+
this.namedMutexOwner.Start();
51+
this.Name = name;
52+
}
53+
54+
/// <summary>
55+
/// Gets the name of the mutex.
56+
/// </summary>
57+
public string? Name { get; }
58+
59+
/// <summary>
60+
/// Disposes of the underlying native objects.
61+
/// </summary>
62+
public void Dispose()
63+
{
64+
int priorState = Interlocked.Exchange(ref this.state, STATE_DISPOSED);
65+
if (priorState != STATE_DISPOSED)
66+
{
67+
this.mutexWorkQueue.Add(ExitSentinel);
68+
this.mutexWorkQueue.CompleteAdding();
69+
}
70+
}
71+
72+
/// <inheritdoc cref="EnterAsync(TimeSpan)"/>
73+
public ValueTask<LockReleaser> EnterAsync() => this.EnterAsync(Timeout.InfiniteTimeSpan);
74+
75+
/// <summary>
76+
/// Acquires the mutex asynchronously.
77+
/// </summary>
78+
/// <param name="timeout">The maximum time to wait before timing out. Use <see cref="Timeout.InfiniteTimeSpan"/> for no timeout, or <see cref="TimeSpan.Zero"/> to acquire the mutex only if it is immediately available.</param>
79+
/// <returns>A value whose disposal will release the mutex.</returns>
80+
/// <exception cref="TimeoutException">Thrown from the awaited result if the mutex could not be acquired within the specified timeout.</exception>
81+
/// <exception cref="ArgumentOutOfRangeException">Thrown from the awaited result if the <paramref name="timeout"/> is a negative number other than -1 milliseconds, which represents an infinite timeout.</exception>
82+
/// <exception cref="InvalidOperationException">Thrown if called before a prior call to this method has completed, with its releaser disposed if the mutex was entered.</exception>
83+
public async ValueTask<LockReleaser> EnterAsync(TimeSpan timeout) => await this.TryEnterAsync(timeout) ?? throw new TimeoutException();
84+
85+
/// <summary>
86+
/// Acquires the mutex asynchronously, allowing for timeouts without throwing exceptions.
87+
/// </summary>
88+
/// <param name="timeout">The maximum time to wait before timing out. Use <see cref="Timeout.InfiniteTimeSpan"/> for no timeout, or <see cref="TimeSpan.Zero"/> to acquire the mutex only if it is immediately available.</param>
89+
/// <returns>
90+
/// If the mutex was acquired, the result is a value whose disposal will release the mutex.
91+
/// In the event of a timeout, the result in a <see langword="null" /> value.
92+
/// </returns>
93+
/// <exception cref="ArgumentOutOfRangeException">Thrown from the awaited result if the <paramref name="timeout"/> is a negative number other than -1 milliseconds, which represents an infinite timeout.</exception>
94+
/// <exception cref="InvalidOperationException">Thrown if called before a prior call to this method has completed, with its releaser disposed if the mutex was entered.</exception>
95+
public ValueTask<LockReleaser?> TryEnterAsync(TimeSpan timeout)
96+
{
97+
int priorState = Interlocked.CompareExchange(ref this.state, STATE_HELD_OR_WAITING, STATE_READY);
98+
switch (priorState)
99+
{
100+
case STATE_HELD_OR_WAITING:
101+
throw new InvalidOperationException();
102+
case STATE_DISPOSED:
103+
throw new ObjectDisposedException(this.GetType().FullName);
104+
}
105+
106+
TaskCompletionSource<LockReleaser?> tcs = new();
107+
this.mutexWorkQueue.Add(delegate
108+
{
109+
try
110+
{
111+
if (this.mutex.WaitOne(timeout))
112+
{
113+
tcs.SetResult(new LockReleaser(this));
114+
}
115+
else
116+
{
117+
Assumes.True(Interlocked.CompareExchange(ref this.state, STATE_READY, STATE_HELD_OR_WAITING) == STATE_HELD_OR_WAITING);
118+
tcs.SetResult(null);
119+
}
120+
}
121+
catch (AbandonedMutexException)
122+
{
123+
tcs.SetResult(new LockReleaser(this, abandoned: true));
124+
}
125+
catch (Exception ex)
126+
{
127+
Assumes.True(Interlocked.CompareExchange(ref this.state, STATE_READY, STATE_HELD_OR_WAITING) == STATE_HELD_OR_WAITING);
128+
tcs.SetException(ex);
129+
}
130+
});
131+
132+
return new ValueTask<LockReleaser?>(tcs.Task);
133+
}
134+
135+
private void Release()
136+
{
137+
Assumes.True(Interlocked.CompareExchange(ref this.state, STATE_READY, STATE_HELD_OR_WAITING) == STATE_HELD_OR_WAITING);
138+
this.mutexWorkQueue.Add(this.mutex.ReleaseMutex);
139+
}
140+
141+
private void MutexOwnerThread()
142+
{
143+
try
144+
{
145+
while (!this.mutexWorkQueue.IsCompleted)
146+
{
147+
Action work = this.mutexWorkQueue.Take();
148+
if (work == ExitSentinel)
149+
{
150+
// We use an exit sentinel to avoid an exception having to be thrown and caught on disposal when we call Take() and CompleteAdding() is called.
151+
break;
152+
}
153+
154+
work();
155+
}
156+
}
157+
finally
158+
{
159+
this.mutex.Dispose();
160+
}
161+
}
162+
163+
/// <summary>
164+
/// The value returned from <see cref="EnterAsync(TimeSpan)"/> that must be disposed to release the mutex.
165+
/// </summary>
166+
public struct LockReleaser : IDisposable
167+
{
168+
private AsyncMutex? owner;
169+
170+
internal LockReleaser(AsyncMutex mutex, bool abandoned = false)
171+
{
172+
this.owner = mutex;
173+
this.IsAbandoned = abandoned;
174+
}
175+
176+
/// <summary>
177+
/// Gets a value indicating whether the mutex was abandoned by its previous owner.
178+
/// </summary>
179+
public bool IsAbandoned { get; }
180+
181+
/// <summary>
182+
/// Releases the named mutex.
183+
/// </summary>
184+
public void Dispose()
185+
{
186+
Interlocked.Exchange(ref this.owner, null)?.Release();
187+
}
188+
}
189+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Microsoft.VisualStudio.Threading.AsyncMutex
2+
Microsoft.VisualStudio.Threading.AsyncMutex.AsyncMutex(string? name) -> void
3+
Microsoft.VisualStudio.Threading.AsyncMutex.Dispose() -> void
4+
Microsoft.VisualStudio.Threading.AsyncMutex.EnterAsync() -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser>
5+
Microsoft.VisualStudio.Threading.AsyncMutex.EnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser>
6+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser
7+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser.Dispose() -> void
8+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser.IsAbandoned.get -> bool
9+
Microsoft.VisualStudio.Threading.AsyncMutex.Name.get -> string?
10+
Microsoft.VisualStudio.Threading.AsyncMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser?>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Microsoft.VisualStudio.Threading.AsyncMutex
2+
Microsoft.VisualStudio.Threading.AsyncMutex.AsyncMutex(string? name) -> void
3+
Microsoft.VisualStudio.Threading.AsyncMutex.Dispose() -> void
4+
Microsoft.VisualStudio.Threading.AsyncMutex.EnterAsync() -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser>
5+
Microsoft.VisualStudio.Threading.AsyncMutex.EnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser>
6+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser
7+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser.Dispose() -> void
8+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser.IsAbandoned.get -> bool
9+
Microsoft.VisualStudio.Threading.AsyncMutex.Name.get -> string?
10+
Microsoft.VisualStudio.Threading.AsyncMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser?>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Microsoft.VisualStudio.Threading.AsyncMutex
2+
Microsoft.VisualStudio.Threading.AsyncMutex.AsyncMutex(string? name) -> void
3+
Microsoft.VisualStudio.Threading.AsyncMutex.Dispose() -> void
4+
Microsoft.VisualStudio.Threading.AsyncMutex.EnterAsync() -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser>
5+
Microsoft.VisualStudio.Threading.AsyncMutex.EnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser>
6+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser
7+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser.Dispose() -> void
8+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser.IsAbandoned.get -> bool
9+
Microsoft.VisualStudio.Threading.AsyncMutex.Name.get -> string?
10+
Microsoft.VisualStudio.Threading.AsyncMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser?>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Microsoft.VisualStudio.Threading.AsyncMutex
2+
Microsoft.VisualStudio.Threading.AsyncMutex.AsyncMutex(string? name) -> void
3+
Microsoft.VisualStudio.Threading.AsyncMutex.Dispose() -> void
4+
Microsoft.VisualStudio.Threading.AsyncMutex.EnterAsync() -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser>
5+
Microsoft.VisualStudio.Threading.AsyncMutex.EnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser>
6+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser
7+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser.Dispose() -> void
8+
Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser.IsAbandoned.get -> bool
9+
Microsoft.VisualStudio.Threading.AsyncMutex.Name.get -> string?
10+
Microsoft.VisualStudio.Threading.AsyncMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.ValueTask<Microsoft.VisualStudio.Threading.AsyncMutex.LockReleaser?>

0 commit comments

Comments
 (0)