Skip to content

Commit 7ab7eaa

Browse files
authored
Support in-process named mutexes in managed implementation (#55199)
* Partially addresses #48720 * Use a dictionary to perform the name to object lookup * Allow multiple handles to refer to a single waitable object * Abandon mutex only when all handles refering to it are closed * Re-enable test cases disabled due to the above issue
1 parent 50413ea commit 7ab7eaa

File tree

5 files changed

+134
-12
lines changed

5 files changed

+134
-12
lines changed

src/libraries/System.IO.IsolatedStorage/tests/AssemblyInfo.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,4 @@
88
// create unique identities for every test to allow every test to have
99
// it's own store.
1010
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)]
11-
[assembly: ActiveIssue("https://github.com/dotnet/runtime/issues/48720", TestPlatforms.AnyUnix, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
1211
[assembly: SkipOnPlatform(TestPlatforms.Browser, "System.IO.IsolatedStorage is not supported on Browser")]

src/libraries/System.Private.CoreLib/src/System/Threading/Mutex.Unix.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ public sealed partial class Mutex
1212
{
1313
private void CreateMutexCore(bool initiallyOwned, string? name, out bool createdNew)
1414
{
15-
// See https://github.com/dotnet/runtime/issues/48720
1615
if (name != null)
1716
{
18-
throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives);
17+
SafeWaitHandle? safeWaitHandle = WaitSubsystem.CreateNamedMutex(initiallyOwned, name, out createdNew);
18+
if (safeWaitHandle == null)
19+
{
20+
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.Threading_WaitHandleCannotBeOpenedException_InvalidHandle, name));
21+
}
22+
SafeWaitHandle = safeWaitHandle;
23+
return;
1924
}
2025

2126
SafeWaitHandle = WaitSubsystem.NewMutex(initiallyOwned);
@@ -24,7 +29,9 @@ private void CreateMutexCore(bool initiallyOwned, string? name, out bool created
2429

2530
private static OpenExistingResult OpenExistingWorker(string name, out Mutex? result)
2631
{
27-
throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives);
32+
OpenExistingResult status = WaitSubsystem.OpenNamedMutex(name, out SafeWaitHandle? safeWaitHandle);
33+
result = status == OpenExistingResult.Success ? new Mutex(safeWaitHandle!) : null;
34+
return status;
2835
}
2936

3037
public void ReleaseMutex()

src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.Unix.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,53 @@ public static SafeWaitHandle NewMutex(bool initiallyOwned)
166166
return safeWaitHandle;
167167
}
168168

169+
public static SafeWaitHandle? CreateNamedMutex(bool initiallyOwned, string name, out bool createdNew)
170+
{
171+
// For initially owned, newly created named mutexes, there is a potential race
172+
// between adding the mutex to the named object table and initially acquiring it.
173+
// To avoid the possibility of another thread retrieving the mutex via its name
174+
// before we managed to acquire it, we perform both steps while holding s_lock.
175+
s_lock.Acquire();
176+
bool holdingLock = true;
177+
try
178+
{
179+
WaitableObject? waitableObject = WaitableObject.CreateNamedMutex_Locked(name, out createdNew);
180+
if (waitableObject == null)
181+
{
182+
return null;
183+
}
184+
SafeWaitHandle safeWaitHandle = NewHandle(waitableObject);
185+
if (!initiallyOwned || !createdNew)
186+
{
187+
return safeWaitHandle;
188+
}
189+
190+
// Acquire the mutex. A thread's <see cref="ThreadWaitInfo"/> has a reference to all <see cref="Mutex"/>es locked
191+
// by the thread. See <see cref="ThreadWaitInfo.LockedMutexesHead"/>. So, acquire the lock only after all
192+
// possibilities for exceptions have been exhausted.
193+
ThreadWaitInfo waitInfo = Thread.CurrentThread.WaitInfo;
194+
int status = waitableObject.Wait_Locked(waitInfo, timeoutMilliseconds: 0, interruptible: false, prioritize: false);
195+
Debug.Assert(status == 0);
196+
// Wait_Locked has already released s_lock, so we no longer hold it here.
197+
holdingLock = false;
198+
return safeWaitHandle;
199+
}
200+
finally
201+
{
202+
if (holdingLock)
203+
{
204+
s_lock.Release();
205+
}
206+
}
207+
}
208+
209+
public static OpenExistingResult OpenNamedMutex(string name, out SafeWaitHandle? result)
210+
{
211+
OpenExistingResult status = WaitableObject.OpenNamedMutex(name, out WaitableObject? mutex);
212+
result = status == OpenExistingResult.Success ? NewHandle(mutex!) : null;
213+
return status;
214+
}
215+
169216
public static void DeleteHandle(IntPtr handle)
170217
{
171218
HandleManager.DeleteHandle(handle);

src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.WaitableObject.Unix.cs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Generic;
45
using System.Diagnostics;
56

67
namespace System.Threading
@@ -14,9 +15,17 @@ internal static partial class WaitSubsystem
1415
/// </summary>
1516
public sealed class WaitableObject
1617
{
18+
/// <summary>
19+
/// Dictionary to look up named waitable objects. This implementation only supports in-process
20+
/// named waitable objects. Currently only named mutexes are supported.
21+
/// </summary>
22+
private static Dictionary<string, WaitableObject>? s_namedObjects;
23+
1724
private readonly WaitableObjectType _type;
1825
private int _signalCount;
1926
private readonly int _maximumSignalCount;
27+
private int _referenceCount;
28+
private readonly string? _name;
2029

2130
/// <summary>
2231
/// Only <see cref="Mutex"/> has a thread ownership requirement, and it's a less common type to be used, so
@@ -33,6 +42,7 @@ private WaitableObject(
3342
WaitableObjectType type,
3443
int initialSignalCount,
3544
int maximumSignalCount,
45+
string? name,
3646
OwnershipInfo? ownershipInfo)
3747
{
3848
Debug.Assert(initialSignalCount >= 0);
@@ -42,6 +52,8 @@ private WaitableObject(
4252
_type = type;
4353
_signalCount = initialSignalCount;
4454
_maximumSignalCount = maximumSignalCount;
55+
_referenceCount = 1;
56+
_name = name;
4557
_ownershipInfo = ownershipInfo;
4658
}
4759

@@ -56,24 +68,87 @@ public static WaitableObject NewEvent(bool initiallySignaled, EventResetMode res
5668
: WaitableObjectType.AutoResetEvent,
5769
initiallySignaled ? 1 : 0,
5870
1,
71+
null,
5972
null);
6073
}
6174

6275
public static WaitableObject NewSemaphore(int initialSignalCount, int maximumSignalCount)
6376
{
64-
return new WaitableObject(WaitableObjectType.Semaphore, initialSignalCount, maximumSignalCount, null);
77+
return new WaitableObject(WaitableObjectType.Semaphore, initialSignalCount, maximumSignalCount, null, null);
6578
}
6679

6780
public static WaitableObject NewMutex()
6881
{
69-
return new WaitableObject(WaitableObjectType.Mutex, 1, 1, new OwnershipInfo());
82+
return new WaitableObject(WaitableObjectType.Mutex, 1, 1, null, new OwnershipInfo());
83+
}
84+
85+
public static WaitableObject? CreateNamedMutex_Locked(string name, out bool createdNew)
86+
{
87+
s_lock.VerifyIsLocked();
88+
89+
s_namedObjects ??= new Dictionary<string, WaitableObject>();
90+
91+
if (s_namedObjects.TryGetValue(name, out WaitableObject? result))
92+
{
93+
createdNew = false;
94+
if (!result.IsMutex)
95+
{
96+
return null;
97+
}
98+
result._referenceCount++;
99+
}
100+
else
101+
{
102+
createdNew = true;
103+
result = new WaitableObject(WaitableObjectType.Mutex, 1, 1, name, new OwnershipInfo());
104+
s_namedObjects.Add(name, result);
105+
}
106+
107+
return result;
108+
}
109+
110+
public static OpenExistingResult OpenNamedMutex(string name, out WaitableObject? result)
111+
{
112+
s_lock.Acquire();
113+
try
114+
{
115+
if (s_namedObjects == null || !s_namedObjects.TryGetValue(name, out result))
116+
{
117+
result = null;
118+
return OpenExistingResult.NameNotFound;
119+
}
120+
if (!result.IsMutex)
121+
{
122+
result = null;
123+
return OpenExistingResult.NameInvalid;
124+
}
125+
result._referenceCount++;
126+
return OpenExistingResult.Success;
127+
}
128+
finally
129+
{
130+
s_lock.Release();
131+
}
70132
}
71133

72134
public void OnDeleteHandle()
73135
{
74136
s_lock.Acquire();
75137
try
76138
{
139+
// Multiple handles may refer to the same named object. Make sure the object
140+
// is only abandoned once the last handle to it is deleted. Also, remove the
141+
// object from the named objects dictionary at this point.
142+
_referenceCount--;
143+
if (_referenceCount > 0)
144+
{
145+
return;
146+
}
147+
if (_name != null)
148+
{
149+
s_namedObjects!.Remove(_name);
150+
}
151+
77152
if (IsMutex && !IsSignaled)
78153
{
79154
// A thread has a reference to all <see cref="Mutex"/>es locked by it, see

src/libraries/System.Threading/tests/MutexTests.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ public void Ctor_InvalidNames_Unix()
4545
}
4646

4747
[Theory]
48-
[ActiveIssue("https://github.com/dotnet/runtime/issues/48720", TestPlatforms.AnyUnix, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
4948
[MemberData(nameof(GetValidNames))]
5049
public void Ctor_ValidName(string name)
5150
{
@@ -97,7 +96,6 @@ public void Ctor_TryCreateGlobalMutexTest_Uwp()
9796
}
9897

9998
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
100-
[ActiveIssue("https://github.com/dotnet/runtime/issues/48720", TestPlatforms.AnyUnix, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
10199
[MemberData(nameof(GetValidNames))]
102100
public void OpenExisting(string name)
103101
{
@@ -132,7 +130,6 @@ public void OpenExisting_InvalidNames()
132130
}
133131

134132
[Fact]
135-
[ActiveIssue("https://github.com/dotnet/runtime/issues/48720", TestPlatforms.AnyUnix, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
136133
public void OpenExisting_UnavailableName()
137134
{
138135
string name = Guid.NewGuid().ToString("N");
@@ -228,7 +225,6 @@ public static IEnumerable<object[]> AbandonExisting_MemberData()
228225
}
229226

230227
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
231-
[ActiveIssue("https://github.com/dotnet/runtime/issues/48720", TestPlatforms.AnyUnix, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
232228
[MemberData(nameof(AbandonExisting_MemberData))]
233229
public void AbandonExisting(
234230
string name,
@@ -469,7 +465,6 @@ private static void IncrementValueInFileNTimes(Mutex mutex, string fileName, int
469465
}
470466

471467
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
472-
[ActiveIssue("https://github.com/dotnet/runtime/issues/48720", TestPlatforms.AnyUnix, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
473468
public void NamedMutex_ThreadExitDisposeRaceTest()
474469
{
475470
var mutexName = Guid.NewGuid().ToString("N");
@@ -530,7 +525,6 @@ public void NamedMutex_ThreadExitDisposeRaceTest()
530525
}
531526

532527
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
533-
[ActiveIssue("https://github.com/dotnet/runtime/issues/48720", TestPlatforms.AnyUnix, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
534528
public void NamedMutex_DisposeWhenLockedRaceTest()
535529
{
536530
var mutexName = Guid.NewGuid().ToString("N");

0 commit comments

Comments
 (0)