Skip to content

Commit

Permalink
Support in-process named mutexes in managed implementation (#55199)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
uweigand authored Jul 8, 2021
1 parent 50413ea commit 7ab7eaa
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@
// create unique identities for every test to allow every test to have
// it's own store.
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)]
[assembly: ActiveIssue("https://github.com/dotnet/runtime/issues/48720", TestPlatforms.AnyUnix, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
[assembly: SkipOnPlatform(TestPlatforms.Browser, "System.IO.IsolatedStorage is not supported on Browser")]
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ public sealed partial class Mutex
{
private void CreateMutexCore(bool initiallyOwned, string? name, out bool createdNew)
{
// See https://github.com/dotnet/runtime/issues/48720
if (name != null)
{
throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives);
SafeWaitHandle? safeWaitHandle = WaitSubsystem.CreateNamedMutex(initiallyOwned, name, out createdNew);
if (safeWaitHandle == null)
{
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.Threading_WaitHandleCannotBeOpenedException_InvalidHandle, name));
}
SafeWaitHandle = safeWaitHandle;
return;
}

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

private static OpenExistingResult OpenExistingWorker(string name, out Mutex? result)
{
throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives);
OpenExistingResult status = WaitSubsystem.OpenNamedMutex(name, out SafeWaitHandle? safeWaitHandle);
result = status == OpenExistingResult.Success ? new Mutex(safeWaitHandle!) : null;
return status;
}

public void ReleaseMutex()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,53 @@ public static SafeWaitHandle NewMutex(bool initiallyOwned)
return safeWaitHandle;
}

public static SafeWaitHandle? CreateNamedMutex(bool initiallyOwned, string name, out bool createdNew)
{
// For initially owned, newly created named mutexes, there is a potential race
// between adding the mutex to the named object table and initially acquiring it.
// To avoid the possibility of another thread retrieving the mutex via its name
// before we managed to acquire it, we perform both steps while holding s_lock.
s_lock.Acquire();
bool holdingLock = true;
try
{
WaitableObject? waitableObject = WaitableObject.CreateNamedMutex_Locked(name, out createdNew);
if (waitableObject == null)
{
return null;
}
SafeWaitHandle safeWaitHandle = NewHandle(waitableObject);
if (!initiallyOwned || !createdNew)
{
return safeWaitHandle;
}

// Acquire the mutex. A thread's <see cref="ThreadWaitInfo"/> has a reference to all <see cref="Mutex"/>es locked
// by the thread. See <see cref="ThreadWaitInfo.LockedMutexesHead"/>. So, acquire the lock only after all
// possibilities for exceptions have been exhausted.
ThreadWaitInfo waitInfo = Thread.CurrentThread.WaitInfo;
int status = waitableObject.Wait_Locked(waitInfo, timeoutMilliseconds: 0, interruptible: false, prioritize: false);
Debug.Assert(status == 0);
// Wait_Locked has already released s_lock, so we no longer hold it here.
holdingLock = false;
return safeWaitHandle;
}
finally
{
if (holdingLock)
{
s_lock.Release();
}
}
}

public static OpenExistingResult OpenNamedMutex(string name, out SafeWaitHandle? result)
{
OpenExistingResult status = WaitableObject.OpenNamedMutex(name, out WaitableObject? mutex);
result = status == OpenExistingResult.Success ? NewHandle(mutex!) : null;
return status;
}

public static void DeleteHandle(IntPtr handle)
{
HandleManager.DeleteHandle(handle);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;

namespace System.Threading
Expand All @@ -14,9 +15,17 @@ internal static partial class WaitSubsystem
/// </summary>
public sealed class WaitableObject
{
/// <summary>
/// Dictionary to look up named waitable objects. This implementation only supports in-process
/// named waitable objects. Currently only named mutexes are supported.
/// </summary>
private static Dictionary<string, WaitableObject>? s_namedObjects;

private readonly WaitableObjectType _type;
private int _signalCount;
private readonly int _maximumSignalCount;
private int _referenceCount;
private readonly string? _name;

/// <summary>
/// Only <see cref="Mutex"/> has a thread ownership requirement, and it's a less common type to be used, so
Expand All @@ -33,6 +42,7 @@ private WaitableObject(
WaitableObjectType type,
int initialSignalCount,
int maximumSignalCount,
string? name,
OwnershipInfo? ownershipInfo)
{
Debug.Assert(initialSignalCount >= 0);
Expand All @@ -42,6 +52,8 @@ private WaitableObject(
_type = type;
_signalCount = initialSignalCount;
_maximumSignalCount = maximumSignalCount;
_referenceCount = 1;
_name = name;
_ownershipInfo = ownershipInfo;
}

Expand All @@ -56,24 +68,87 @@ public static WaitableObject NewEvent(bool initiallySignaled, EventResetMode res
: WaitableObjectType.AutoResetEvent,
initiallySignaled ? 1 : 0,
1,
null,
null);
}

public static WaitableObject NewSemaphore(int initialSignalCount, int maximumSignalCount)
{
return new WaitableObject(WaitableObjectType.Semaphore, initialSignalCount, maximumSignalCount, null);
return new WaitableObject(WaitableObjectType.Semaphore, initialSignalCount, maximumSignalCount, null, null);
}

public static WaitableObject NewMutex()
{
return new WaitableObject(WaitableObjectType.Mutex, 1, 1, new OwnershipInfo());
return new WaitableObject(WaitableObjectType.Mutex, 1, 1, null, new OwnershipInfo());
}

public static WaitableObject? CreateNamedMutex_Locked(string name, out bool createdNew)
{
s_lock.VerifyIsLocked();

s_namedObjects ??= new Dictionary<string, WaitableObject>();

if (s_namedObjects.TryGetValue(name, out WaitableObject? result))
{
createdNew = false;
if (!result.IsMutex)
{
return null;
}
result._referenceCount++;
}
else
{
createdNew = true;
result = new WaitableObject(WaitableObjectType.Mutex, 1, 1, name, new OwnershipInfo());
s_namedObjects.Add(name, result);
}

return result;
}

public static OpenExistingResult OpenNamedMutex(string name, out WaitableObject? result)
{
s_lock.Acquire();
try
{
if (s_namedObjects == null || !s_namedObjects.TryGetValue(name, out result))
{
result = null;
return OpenExistingResult.NameNotFound;
}
if (!result.IsMutex)
{
result = null;
return OpenExistingResult.NameInvalid;
}
result._referenceCount++;
return OpenExistingResult.Success;
}
finally
{
s_lock.Release();
}
}

public void OnDeleteHandle()
{
s_lock.Acquire();
try
{
// Multiple handles may refer to the same named object. Make sure the object
// is only abandoned once the last handle to it is deleted. Also, remove the
// object from the named objects dictionary at this point.
_referenceCount--;
if (_referenceCount > 0)
{
return;
}
if (_name != null)
{
s_namedObjects!.Remove(_name);
}

if (IsMutex && !IsSignaled)
{
// A thread has a reference to all <see cref="Mutex"/>es locked by it, see
Expand Down
6 changes: 0 additions & 6 deletions src/libraries/System.Threading/tests/MutexTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ public void Ctor_InvalidNames_Unix()
}

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

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

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

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

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

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

0 comments on commit 7ab7eaa

Please sign in to comment.