|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace System.Threading
{
/// # Wait subsystem
///
/// ## Types
///
/// <see cref="WaitSubsystem"/>
/// - Static API surface for dealing with synchronization objects that support multi-wait, and to put a thread into a wait
/// state, on Unix
/// - Any interaction with the wait subsystem from outside should go through APIs on this class, and should not directly
/// go through any of the nested classes
///
/// <see cref="WaitableObject"/>
/// - An object that supports the features of <see cref="EventWaitHandle"/>, <see cref="Semaphore"/>, and
/// <see cref="Mutex"/>. The handle of each of those classes is associated with a <see cref="WaitableObject"/>.
///
/// <see cref="ThreadWaitInfo"/>
/// - Keeps information about a thread's wait and provides functionlity to put a thread into a wait state and to take it
/// out of a wait state. Each thread has an instance available through <see cref="Thread.WaitInfo"/>.
///
/// <see cref="HandleManager"/>
/// - Provides functionality to allocate a handle associated with a <see cref="WaitableObject"/>, to retrieve the object
/// from a handle, and to delete a handle.
///
/// <see cref="LowLevelLock"/> and <see cref="LowLevelMonitor"/>
/// - These are "low level" in the sense they don't depend on this wait subsystem, and any waits done are not
/// interruptible
/// - <see cref="LowLevelLock"/> is used for the process-wide lock <see cref="s_lock"/>
/// - <see cref="LowLevelMonitor"/> is the main system dependency of the wait subsystem, and all waits are done through
/// it. It is backed by a C++ equivalent in CoreLib.Native's pal_threading.*, which wraps a pthread mutex/condition
/// pair. Each thread has an instance in <see cref="ThreadWaitInfo._waitMonitor"/>, which is used to synchronize the
/// thread's wait state and for waiting. <see cref="LowLevelLock"/> also uses an instance of
/// <see cref="LowLevelMonitor"/> for waiting.
///
/// ## Design goals
///
/// Behave similarly to wait operations on Windows
/// - Waiting
/// - A waiter keeps an array of objects on which it is waiting (see <see cref="ThreadWaitInfo._waitedObjects"/>).
/// - The waiter registers a <see cref="ThreadWaitInfo.WaitedListNode"/> with each <see cref="WaitableObject"/>
/// - The waiter waits on its own <see cref="ThreadWaitInfo._waitMonitor"/> to go into a wait state
/// - Upon timeout, the waiter unregisters the wait and continues
/// - Sleeping
/// - Sleeping is just another way of waiting, only there would not be any waited objects
/// - Signaling
/// - A signaler iterates over waiters and tries to release waiters based on the signal count
/// - For each waiter, the signaler checks if the waiter's wait can be terminated
/// - When a waiter's wait can be terminated, the signaler does everything necessary before waking the waiter, such that
/// the waiter can simply continue after awakening, including unregistering the wait and assigning ownership if
/// applicable
/// - Interrupting
/// - Interrupting is just another way of signaling a waiting thread. The interrupter unregisters the wait and wakes the
/// waiter.
/// - Wait release fairness
/// - As mentioned above in how signaling works, waiters are released in fair order (first come, first served)
/// - This is mostly done to match the behavior of synchronization objects in Windows, which are also fair
/// - Events have an implicit requirement to be fair
/// - For a <see cref="ManualResetEvent"/>, Set/Reset in quick succession requires that it wakes up all waiters,
/// implying that the design cannot be to signal a thread to wake and have it check the state when it awakens some
/// time in the future
/// - For an <see cref="AutoResetEvent"/>, Set/Set in quick succession requires that it wakes up two threads, implying
/// that a Set/Wait in quick succession cannot have the calling thread accept its own signal if there is a waiter
/// - There is an advantage to being fair, as it guarantees that threads are only awakened when necessary. That is, a
/// thread will never wake up and find that it has to go back to sleep because the wait is not satisfied (except due
/// to spurious wakeups caused by external factors).
/// - Synchronization
/// - A process-wide lock <see cref="s_lock"/> is used to synchronize most operations and the signal state of all
/// <see cref="WaitableObject"/>s in the process. Given that it is recommended to use alternative synchronization
/// types (<see cref="ManualResetEventSlim"/>, <see cref="SemaphoreSlim"/>, <see cref="Monitor"/>) for single-wait
/// cases, it is probably not worth optimizing for the single-wait case. It is possible with a small design change to
/// bypass the lock and use interlocked operations for uncontended cases, but at the cost of making multi-waits more
/// complicated and slower.
/// - The wait state of a thread (<see cref="ThreadWaitInfo._waitSignalState"/>), among other things, is synchronized
/// using the thread's <see cref="ThreadWaitInfo._waitMonitor"/>, so signalers and interrupters acquire the monitor's
/// lock before checking the wait state of a thread and signaling the thread to wake up.
///
/// Self-consistent in the event of any exception
/// - Try/finally is used extensively, including around any operation that could fail due to out-of-memory
///
/// Decent balance between memory usage and performance
/// - <see cref="WaitableObject"/> is intended to be as small as possible while avoiding virtual calls and casts
/// - As <see cref="Mutex"/> is not commonly used and requires more state, some of its state is separated into
/// <see cref="WaitableObject._ownershipInfo"/>
/// - When support for cross-process objects is added, the current thought is to have an <see cref="object"/> field that
/// is used for both cross-process state and ownership state.
///
/// No allocation in typical cases of any operation except where necessary
/// - Since the maximum number of wait handles for a multi-wait operation is limited to
/// <see cref="WaitHandle.MaxWaitHandles"/>, arrays necessary for holding information about a multi-wait, and list nodes
/// necessary for registering a wait, are precreated with a low initial capacity that covers most typical cases
/// - Threads track owned mutexes by linking the <see cref="WaitableObject.OwnershipInfo"/> instance into a linked list
/// <see cref="ThreadWaitInfo.LockedMutexesHead"/>. <see cref="WaitableObject.OwnershipInfo"/> is itself a list node,
/// and is created along with the mutex <see cref="WaitableObject"/>.
///
/// Minimal p/invokes in typical uncontended cases
/// - <see cref="HandleManager"/> currently uses <see cref="Runtime.InteropServices.GCHandle"/> in the interest of
/// simplicity, which p/invokes and does a cast to get the <see cref="WaitableObject"/> from a handle
/// - Most of the wait subsystem is written in C#, so there is no initially required p/invoke
/// - <see cref="LowLevelLock"/>, used by the process-wide lock <see cref="s_lock"/>, uses interlocked operations to
/// acquire and release the lock when there is no need to wait or to release a waiter. This is significantly faster than
/// using <see cref="LowLevelMonitor"/> as a lock, which uses pthread mutex functionality through p/invoke. The lock is
/// typically not held for very long, especially since allocations inside the lock will be rare.
/// - Since <see cref="s_lock"/> provides mutual exclusion for the states of all <see cref="WaitableObject"/>s in the
/// process, any operation that does not involve waiting or releasing a wait can occur with minimal p/invokes
///
#if NATIVEAOT
[EagerStaticClassConstruction] // the wait subsystem is used during lazy class construction
#endif
internal static partial class WaitSubsystem
{
private static readonly LowLevelLock s_lock = new LowLevelLock();
// Exception handling may use the WaitSubsystem. It means that we need to release the WaitSubsystem
// lock before throwing any exceptions to avoid deadlocks. LockHolder allows us to pass the lock state
// around and keep track of whether the lock still needs to be released.
public struct LockHolder
{
private LowLevelLock? _lock;
public LockHolder(LowLevelLock l)
{
l.Acquire();
_lock = l;
}
public void Dispose()
{
if (_lock != null)
{
_lock.Release();
_lock = null;
}
}
}
#pragma warning disable CA1859 // Change type of parameter 'waitableObject' from 'System.Threading.IWaitableObject' to 'System.Threading.WaitableObject' for improved performance
// Some platforms have more implementations of IWaitableObject than just WaitableObject.
private static SafeWaitHandle NewHandle(IWaitableObject waitableObject)
#pragma warning restore CA1859 // Change type of parameter 'waitableObject' from 'System.Threading.IWaitableObject' to 'System.Threading.WaitableObject' for improved performance
{
var safeWaitHandle = new SafeWaitHandle();
IntPtr handle = IntPtr.Zero;
try
{
handle = HandleManager.NewHandle(waitableObject);
}
finally
{
if (handle == IntPtr.Zero)
{
waitableObject.OnDeleteHandle();
}
}
Marshal.InitHandle(safeWaitHandle, handle);
return safeWaitHandle;
}
public static SafeWaitHandle NewEvent(bool initiallySignaled, EventResetMode resetMode)
{
return NewHandle(WaitableObject.NewEvent(initiallySignaled, resetMode));
}
public static SafeWaitHandle NewSemaphore(int initialSignalCount, int maximumSignalCount)
{
return NewHandle(WaitableObject.NewSemaphore(initialSignalCount, maximumSignalCount));
}
public static SafeWaitHandle NewMutex(bool initiallyOwned)
{
WaitableObject waitableObject = WaitableObject.NewMutex();
SafeWaitHandle safeWaitHandle = NewHandle(waitableObject);
if (!initiallyOwned)
{
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;
bool acquiredLock = waitableObject.Wait(waitInfo, timeoutMilliseconds: 0, interruptible: false, prioritize: false) == 0;
Debug.Assert(acquiredLock);
return safeWaitHandle;
}
#if FEATURE_CROSS_PROCESS_MUTEX
public static SafeWaitHandle? CreateNamedMutex(bool initiallyOwned, string name, bool isUserScope, out bool createdNew)
{
NamedMutex? namedMutex = NamedMutex.CreateNamedMutex(name, isUserScope, initiallyOwned: initiallyOwned, out createdNew);
if (namedMutex == null)
{
return null;
}
SafeWaitHandle safeWaitHandle = NewHandle(namedMutex);
return safeWaitHandle;
}
public static OpenExistingResult OpenNamedMutex(string name, bool isUserScope, out SafeWaitHandle? result)
{
OpenExistingResult status = NamedMutex.OpenNamedMutex(name, isUserScope, out NamedMutex? mutex);
result = status == OpenExistingResult.Success ? NewHandle(mutex!) : null;
return status;
}
#else
public static SafeWaitHandle? CreateNamedMutex(bool initiallyOwned, string name, bool isUserScope, 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.
LockHolder lockHolder = new LockHolder(s_lock);
try
{
if (isUserScope)
{
name = $"User\\{name}";
}
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, ref lockHolder);
Debug.Assert(status == 0);
return safeWaitHandle;
}
finally
{
lockHolder.Dispose();
}
}
public static OpenExistingResult OpenNamedMutex(string name, bool isUserScope, out SafeWaitHandle? result)
{
if (isUserScope)
{
name = $"User\\{name}";
}
OpenExistingResult status = WaitableObject.OpenNamedMutex(name, out WaitableObject? mutex);
result = status == OpenExistingResult.Success ? NewHandle(mutex!) : null;
return status;
}
#endif
public static void DeleteHandle(IntPtr handle)
{
HandleManager.DeleteHandle(handle);
}
public static void SetEvent(IntPtr handle)
{
SetEvent((WaitableObject)HandleManager.FromHandle(handle));
}
public static void SetEvent(WaitableObject waitableObject)
{
Debug.Assert(waitableObject != null);
LockHolder lockHolder = new LockHolder(s_lock);
try
{
waitableObject.SignalEvent(ref lockHolder);
}
finally
{
lockHolder.Dispose();
}
}
public static void ResetEvent(IntPtr handle)
{
ResetEvent((WaitableObject)HandleManager.FromHandle(handle));
}
public static void ResetEvent(WaitableObject waitableObject)
{
Debug.Assert(waitableObject != null);
LockHolder lockHolder = new LockHolder(s_lock);
try
{
waitableObject.UnsignalEvent(ref lockHolder);
}
finally
{
lockHolder.Dispose();
}
}
public static int ReleaseSemaphore(IntPtr handle, int count)
{
Debug.Assert(count > 0);
return ReleaseSemaphore((WaitableObject)HandleManager.FromHandle(handle), count);
}
public static int ReleaseSemaphore(WaitableObject waitableObject, int count)
{
Debug.Assert(waitableObject != null);
Debug.Assert(count > 0);
LockHolder lockHolder = new LockHolder(s_lock);
try
{
return waitableObject.SignalSemaphore(count, ref lockHolder);
}
finally
{
lockHolder.Dispose();
}
}
public static void ReleaseMutex(IntPtr handle)
{
ReleaseMutex(HandleManager.FromHandle(handle));
}
public static void ReleaseMutex(IWaitableObject waitableObject)
{
Debug.Assert(waitableObject != null);
LockHolder lockHolder = new LockHolder(s_lock);
try
{
waitableObject.Signal(1, ref lockHolder);
}
finally
{
lockHolder.Dispose();
}
}
public static int Wait(IntPtr handle, int timeoutMilliseconds, bool interruptible)
{
Debug.Assert(timeoutMilliseconds >= -1);
return Wait(HandleManager.FromHandle(handle), timeoutMilliseconds, interruptible);
}
public static int Wait(
IWaitableObject waitableObject,
int timeoutMilliseconds,
bool interruptible = true,
bool prioritize = false)
{
Debug.Assert(waitableObject != null);
Debug.Assert(timeoutMilliseconds >= -1);
return waitableObject.Wait(Thread.CurrentThread.WaitInfo, timeoutMilliseconds, interruptible, prioritize);
}
public static int Wait(
ReadOnlySpan<IntPtr> waitHandles,
bool waitForAll,
int timeoutMilliseconds)
{
Debug.Assert(waitHandles.Length > 0);
Debug.Assert(waitHandles.Length <= WaitHandle.MaxWaitHandles);
Debug.Assert(timeoutMilliseconds >= -1);
ThreadWaitInfo waitInfo = Thread.CurrentThread.WaitInfo;
#if FEATURE_CROSS_PROCESS_MUTEX
if (waitHandles.Length == 1 && HandleManager.FromHandle(waitHandles[0]) is NamedMutex namedMutex)
{
// Named mutexes don't participate in the wait subsystem fully.
return namedMutex.Wait(waitInfo, timeoutMilliseconds, interruptible: true, prioritize: false);
}
#endif
WaitableObject?[] waitableObjects = waitInfo.GetWaitedObjectArray(waitHandles.Length);
bool success = false;
try
{
for (int i = 0; i < waitHandles.Length; ++i)
{
Debug.Assert(waitHandles[i] != IntPtr.Zero);
IWaitableObject waitableObjectMaybe = HandleManager.FromHandle(waitHandles[i]);
if (waitableObjectMaybe is not WaitableObject waitableObject)
{
throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSyncObjectWaitAnyWaitAll);
}
if (waitForAll)
{
// Check if this is a duplicate, as wait-for-all does not support duplicates. Including the parent
// loop, this becomes a brute force O(n^2) search, which is intended since the typical array length is
// short enough that this would actually be faster than other alternatives. Also, the worst case is not
// so bad considering that the array length is limited by <see cref="WaitHandle.MaxWaitHandles"/>.
for (int j = 0; j < i; ++j)
{
if (waitableObject == waitableObjects[j])
{
throw new DuplicateWaitObjectException("waitHandles[" + i + ']');
}
}
}
waitableObjects[i] = waitableObject;
}
success = true;
}
finally
{
if (!success)
{
for (int i = 0; i < waitHandles.Length; ++i)
{
waitableObjects[i] = null;
}
}
}
if (waitHandles.Length == 1)
{
WaitableObject waitableObject = waitableObjects[0]!;
waitableObjects[0] = null;
return
waitableObject.Wait(waitInfo, timeoutMilliseconds, interruptible: true, prioritize: false);
}
return
WaitableObject.Wait(
waitableObjects,
waitHandles.Length,
waitForAll,
waitInfo,
timeoutMilliseconds,
interruptible: true,
prioritize: false);
}
public static int SignalAndWait(
IntPtr handleToSignal,
IntPtr handleToWaitOn,
int timeoutMilliseconds)
{
Debug.Assert(timeoutMilliseconds >= -1);
return
SignalAndWait(
HandleManager.FromHandle(handleToSignal),
HandleManager.FromHandle(handleToWaitOn),
timeoutMilliseconds);
}
public static int SignalAndWait(
IWaitableObject waitableObjectToSignal,
IWaitableObject waitableObjectToWaitOn,
int timeoutMilliseconds,
bool interruptible = true,
bool prioritize = false)
{
Debug.Assert(waitableObjectToSignal != null);
Debug.Assert(waitableObjectToWaitOn != null);
Debug.Assert(timeoutMilliseconds >= -1);
ThreadWaitInfo waitInfo = Thread.CurrentThread.WaitInfo;
LockHolder lockHolder = new LockHolder(s_lock);
try
{
// A pending interrupt does not signal the specified handle
if (interruptible && waitInfo.CheckAndResetPendingInterrupt)
{
lockHolder.Dispose();
throw new ThreadInterruptedException();
}
try
{
waitableObjectToSignal.Signal(1, ref lockHolder);
}
catch (SemaphoreFullException ex)
{
s_lock.VerifyIsNotLocked();
throw new InvalidOperationException(SR.Threading_WaitHandleTooManyPosts, ex);
}
return waitableObjectToWaitOn.Wait_Locked(waitInfo, timeoutMilliseconds, interruptible, prioritize, ref lockHolder);
}
finally
{
lockHolder.Dispose();
}
}
public static void UninterruptibleSleep0()
{
ThreadWaitInfo.UninterruptibleSleep0();
}
public static void Sleep(int timeoutMilliseconds, bool interruptible = true)
{
ThreadWaitInfo.Sleep(timeoutMilliseconds, interruptible);
}
public static void Interrupt(Thread thread)
{
Debug.Assert(thread != null);
s_lock.Acquire();
try
{
thread.WaitInfo.TrySignalToInterruptWaitOrRecordPendingInterrupt();
}
finally
{
s_lock.Release();
}
}
}
}
|