From 94121ed6a65609805eb5539739ca787299d9a890 Mon Sep 17 00:00:00 2001
From: vsadov <8218165+VSadov@users.noreply.github.com>
Date: Thu, 4 Jun 2026 11:12:59 -0700
Subject: [PATCH 01/20] timeout tweak
---
.../src/System/Threading/ManualResetEventSlim.cs | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs
index c07fb07a8328d3..bb003eb1d8bbfa 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs
@@ -502,7 +502,6 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
// We spin briefly before falling back to allocating and/or waiting on a true event.
long startTime = 0;
- bool bNeedTimeoutAdjustment = false;
int realMillisecondsTimeout = millisecondsTimeout; // this will be adjusted if necessary.
if (millisecondsTimeout != Timeout.Infinite)
@@ -514,7 +513,6 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
// decide to block in the kernel below.
startTime = Environment.TickCount64;
- bNeedTimeoutAdjustment = true;
}
// Spin
@@ -549,7 +547,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
cancellationToken.ThrowIfCancellationRequested();
// update timeout (delays in wait commencement are due to spinning and/or spurious wakeups from other waits being canceled)
- if (bNeedTimeoutAdjustment)
+ if (startTime != 0)
{
// TimeoutHelper.UpdateTimeOut returns a long but the value is capped as millisecondsTimeout is an int.
realMillisecondsTimeout = (int)TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout);
From 6b561358d8ef91017b52bef624152fb1f82d8755 Mon Sep 17 00:00:00 2001
From: vsadov <8218165+VSadov@users.noreply.github.com>
Date: Thu, 4 Jun 2026 14:11:44 -0700
Subject: [PATCH 02/20] use InterlockedIncrement
---
.../System/Threading/ManualResetEventSlim.cs | 116 ++++++------------
1 file changed, 40 insertions(+), 76 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs
index bb003eb1d8bbfa..e7c2b1393bb0b1 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs
@@ -44,11 +44,10 @@ public class ManualResetEventSlim : IDisposable
// -- State -- //
// For a packed word a uint would seem better, but Interlocked.* doesn't support them as uint isn't CLS-compliant.
- private volatile int m_combinedState; // ie a uint. Used for the state items listed below.
+ private int m_combinedState; // ie a uint. Used for the state items listed below.
// 1-bit for signalled state
private const int SignalledState_BitMask = unchecked((int)0x80000000); // 1000 0000 0000 0000 0000 0000 0000 0000
- private const int SignalledState_ShiftCount = 31;
// 1-bit for disposed state
private const int Dispose_BitMask = unchecked((int)0x40000000); // 0100 0000 0000 0000 0000 0000 0000 0000
@@ -60,7 +59,6 @@ public class ManualResetEventSlim : IDisposable
// 19-bits for m_waiters. This allows support of 512K threads waiting which should be ample
private const int NumWaitersState_BitMask = unchecked((int)0x0007FFFF); // 0000 0000 0000 0111 1111 1111 1111 1111
- private const int NumWaitersState_ShiftCount = 0;
private const int NumWaitersState_MaxValue = (1 << 19) - 1; // 512K-1
// ----------- //
@@ -68,7 +66,7 @@ public class ManualResetEventSlim : IDisposable
/// Gets the underlying object for this .
///
- /// The underlying event object fore this The underlying event object for this .
///
/// Accessing this property forces initialization of an underlying event object if one hasn't
@@ -97,8 +95,22 @@ public WaitHandle WaitHandle
/// true if the event has is set; otherwise, false.
public bool IsSet
{
- get => 0 != ExtractStatePortion(m_combinedState, SignalledState_BitMask);
- private set => UpdateStateAtomically(((value) ? 1 : 0) << SignalledState_ShiftCount, SignalledState_BitMask);
+ get
+ {
+ return m_combinedState < 0;
+ }
+
+ private set
+ {
+ if (value)
+ {
+ Interlocked.Or(ref m_combinedState, SignalledState_BitMask);
+ }
+ else
+ {
+ Interlocked.And(ref m_combinedState, ~SignalledState_BitMask);
+ }
+ }
}
///
@@ -116,23 +128,21 @@ private set
}
}
- ///
- /// How many threads are waiting.
- ///
- private int Waiters
+ private void InterlockedIncrementWaiters()
{
- get => ExtractStatePortionAndShiftRight(m_combinedState, NumWaitersState_BitMask, NumWaitersState_ShiftCount);
- set
- {
- // setting to <0 would indicate an internal flaw, hence Assert is appropriate.
- Debug.Assert(value >= 0, "NumWaiters should never be less than zero. This indicates an internal error.");
+ int newWaiters = Interlocked.Increment(ref m_combinedState) & NumWaitersState_BitMask;
- // it is possible for the max number of waiters to be exceeded via user-code, hence we use a real exception here.
- if (value >= NumWaitersState_MaxValue)
- throw new InvalidOperationException(SR.Format(SR.ManualResetEventSlim_ctor_TooManyWaiters, NumWaitersState_MaxValue));
+ // it is possible for the max number of waiters to be exceeded via user-code, hence we use a real exception here.
+ if (newWaiters >= NumWaitersState_MaxValue)
+ throw new InvalidOperationException(SR.Format(SR.ManualResetEventSlim_ctor_TooManyWaiters, NumWaitersState_MaxValue));
+ }
- UpdateStateAtomically(value << NumWaitersState_ShiftCount, NumWaitersState_BitMask);
- }
+ private void InterlockedDecrementWaiters()
+ {
+ int newWaiters = Interlocked.Decrement(ref m_combinedState) & NumWaitersState_BitMask;
+
+ // setting to <0 would indicate an internal flaw, hence Assert is appropriate.
+ Debug.Assert(newWaiters >= 0, "NumWaiters should never be less than zero. This indicates an internal error.");
}
//-----------------------------------------------------------------------------------
@@ -190,7 +200,8 @@ public ManualResetEventSlim(bool initialState, int spinCount)
#pragma warning disable IDE0060 // Remove unused parameter spinCount, on single-threaded systems, the spin count is not used.
private void Initialize(bool initialState, int spinCount)
{
- m_combinedState = initialState ? (1 << SignalledState_ShiftCount) : 0;
+ IsSet = initialState;
+
// the spinCount argument has been validated by the ctors.
// but we now sanity check our predefined constants.
Debug.Assert(DEFAULT_SPIN_SP >= 0, "Internal error - DEFAULT_SPIN_SP is outside the legal range.");
@@ -213,7 +224,7 @@ private void EnsureLockObjectCreated()
}
///
- /// This method lazily initializes the event object. It uses CAS to guarantee that
+ /// This method lazily initializes the public wait object. It uses CAS to guarantee that
/// many threads racing to call this at once don't result in more than one event
/// being stored and used. The event will be signaled or unsignaled depending on
/// the state of the thin-event itself, with synchronization taken into account.
@@ -235,7 +246,7 @@ private void LazyInitializeEvent()
// Now that the event is published, verify that the state hasn't changed since
// we snapped the preInitializeState. Another thread could have done that
// between our initial observation above and here. The barrier incurred from
- // the CAS above (in addition to m_state being volatile) prevents this read
+ // the CAS above prevents this read
// from moving earlier and being collapsed with our original one.
bool currentIsSet = IsSet;
if (currentIsSet != preInitializeIsSet)
@@ -275,10 +286,10 @@ private void Set(bool duringCancellation)
// We need to ensure that IsSet=true does not get reordered past the read of m_eventObj
// This would be a legal movement according to the .NET memory model.
// The code is safe as IsSet involves an Interlocked.CompareExchange which provides a full memory barrier.
- IsSet = true;
+ long origState = Interlocked.Or(ref m_combinedState, SignalledState_BitMask);
- // If there are waiting threads, we need to pulse them.
- if (Waiters > 0)
+ // If there were waiting threads at Set time, we need to pulse them.
+ if ((origState & NumWaitersState_BitMask) != 0)
{
Debug.Assert(m_lock != null); // if waiters>0, then m_lock has already been created.
lock (m_lock)
@@ -563,11 +574,11 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
// If we see IsSet=false, then we are guaranteed that Set() will see that we are
// waiting and will pulse the monitor correctly.
- Waiters++;
+ InterlockedIncrementWaiters();
if (IsSet) // This check must occur after updating Waiters.
{
- Waiters--; // revert the increment.
+ InterlockedDecrementWaiters(); // revert the increment.
return true;
}
@@ -581,7 +592,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
finally
{
// Clean up: we're done waiting.
- Waiters--;
+ InterlockedDecrementWaiters();
}
// Now just loop back around, and the right thing will happen. Either:
// 1. We had a spurious wake-up due to some other wait being canceled via a different cancellationToken (rewait)
@@ -656,38 +667,6 @@ private static void CancellationTokenCallback(object? obj)
}
}
- ///
- /// Private helper method for updating parts of a bit-string state value.
- /// Mainly called from the IsSet and Waiters properties setters
- ///
- ///
- /// Note: the parameter types must be int as CompareExchange cannot take a Uint
- ///
- /// The new value
- /// The mask used to set the bits
- private void UpdateStateAtomically(int newBits, int updateBitsMask)
- {
- SpinWait sw = default;
-
- Debug.Assert((newBits | updateBitsMask) == updateBitsMask, "newBits do not fall within the updateBitsMask.");
-
- while (true)
- {
- int oldState = m_combinedState; // cache the old value for testing in CAS
-
- // Procedure:(1) zero the updateBits. eg oldState = [11111111] flag= [00111000] newState = [11000111]
- // then (2) map in the newBits. eg [11000111] newBits=00101000, newState=[11101111]
- int newState = (oldState & ~updateBitsMask) | newBits;
-
- if (Interlocked.CompareExchange(ref m_combinedState, newState, oldState) == oldState)
- {
- return;
- }
-
- sw.SpinOnce(sleep1Threshold: -1);
- }
- }
-
///
/// Private helper method - performs Mask and shift, particular helpful to extract a field from a packed word.
/// eg ExtractStatePortionAndShiftRight(0x12345678, 0xFF000000, 24) => 0x12, ie extracting the top 8-bits as a simple integer
@@ -704,20 +683,5 @@ private static int ExtractStatePortionAndShiftRight(int state, int mask, int rig
// then convert back to int.
return unchecked((int)(((uint)(state & mask)) >> rightBitShiftCount));
}
-
- ///
- /// Performs a Mask operation, but does not perform the shift.
- /// This is acceptable for boolean values for which the shift is unnecessary
- /// eg (val & Mask) != 0 is an appropriate way to extract a boolean rather than using
- /// ((val & Mask) >> shiftAmount) == 1
- ///
- /// ?? is there a common place to put this rather than being private to MRES?
- ///
- ///
- ///
- private static int ExtractStatePortion(int state, int mask)
- {
- return state & mask;
- }
}
}
From 96984be4d5151e37020a50efa449dbf842adde10 Mon Sep 17 00:00:00 2001
From: vsadov <8218165+VSadov@users.noreply.github.com>
Date: Thu, 4 Jun 2026 21:48:44 -0700
Subject: [PATCH 03/20] remove s_conditionTable
---
.../src/System/Threading/Condition.cs | 5 ++
.../src/System/Threading/Lock.cs | 58 +++++++++++++++----
.../src/System/Threading/Monitor.cs | 8 +--
3 files changed, 56 insertions(+), 15 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Condition.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Condition.cs
index 302721b59d8097..ff77752b05b498 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Condition.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Condition.cs
@@ -47,6 +47,11 @@ private static void ReleaseWaiterForCurrentThread(Waiter waiter)
}
private readonly Lock _lock;
+
+ // When condition is installed in a Lock it takes the same field as waitEvent would.
+ // If waitEvent is also needed it is available through here.
+ internal AutoResetEvent? _waitEvent;
+
private Waiter? _waitersHead;
private Waiter? _waitersTail;
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs
index f050a4c64b9bfa..3795d597290fcb 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs
@@ -52,7 +52,39 @@ public sealed class Lock
private short _spinCount;
private ushort _waiterStartTimeMs;
- private AutoResetEvent? _waitEvent;
+
+ private object? _waitEventOrCondition;
+ private AutoResetEvent? WaitEvent
+ {
+ get
+ {
+ object? weoc = _waitEventOrCondition;
+ if (weoc is Condition c)
+ return c._waitEvent;
+
+ return (AutoResetEvent?)weoc;
+ }
+ }
+
+ internal Condition GetOrCreateCondition()
+ {
+ // The loop terminates as _waitEventOrCondition has limited number of
+ // state transitions with no cycles.
+ while (true)
+ {
+ object? weoc = _waitEventOrCondition;
+ if (weoc is Condition c)
+ return c;
+
+ Condition newCondition = new Condition(this);
+ newCondition._waitEvent = WaitEvent;
+
+ if (Interlocked.CompareExchange(ref _waitEventOrCondition, newCondition, weoc) == weoc)
+ {
+ return newCondition;
+ }
+ }
+ }
///
/// Initializes a new instance of the class.
@@ -513,7 +545,8 @@ internal int TryEnterSlow(int timeoutMs, int currentThreadId)
NativeRuntimeEventSource.Log.IsEnabled(
EventLevel.Informational,
NativeRuntimeEventSource.Keywords.ContentionKeyword);
- AutoResetEvent waitEvent = _waitEvent ?? CreateWaitEvent(areContentionEventsEnabled);
+
+ AutoResetEvent waitEvent = WaitEvent ?? CreateWaitEvent(areContentionEventsEnabled);
if (State.TryLockBeforeWait(this))
{
// Lock was acquired and a waiter was not registered
@@ -652,8 +685,13 @@ private bool ShouldStopPreemptingWaiters
private AutoResetEvent CreateWaitEvent(bool areContentionEventsEnabled)
{
var newWaitEvent = new AutoResetEvent(false);
- AutoResetEvent? waitEventBeforeUpdate = Interlocked.CompareExchange(ref _waitEvent, newWaitEvent, null);
- if (waitEventBeforeUpdate == null)
+ object? weocBeforeUpdate = Interlocked.CompareExchange(ref _waitEventOrCondition, newWaitEvent, null);
+ if (weocBeforeUpdate is Condition c)
+ {
+ weocBeforeUpdate = Interlocked.CompareExchange(ref c._waitEvent, newWaitEvent, null);
+ }
+
+ if (weocBeforeUpdate == null)
{
// Also check NativeRuntimeEventSource.Log.IsEnabled() to enable trimming
if (areContentionEventsEnabled && NativeRuntimeEventSource.Log.IsEnabled())
@@ -665,7 +703,7 @@ private AutoResetEvent CreateWaitEvent(bool areContentionEventsEnabled)
}
newWaitEvent.Dispose();
- return waitEventBeforeUpdate;
+ return (AutoResetEvent)weocBeforeUpdate;
}
[MethodImpl(MethodImplOptions.NoInlining)]
@@ -674,8 +712,8 @@ private void SignalWaiterIfNecessary(State state)
if (State.TrySetIsWaiterSignaledToWake(this, state))
{
// Signal a waiter to wake
- Debug.Assert(_waitEvent != null);
- bool signaled = _waitEvent.Set();
+ Debug.Assert(WaitEvent != null);
+ bool signaled = WaitEvent.Set();
Debug.Assert(signaled);
}
}
@@ -695,14 +733,14 @@ public bool IsHeldByCurrentThread
}
internal static long ContentionCount => s_contentionCount;
- internal void Dispose() => _waitEvent?.Dispose();
+ internal void Dispose() => WaitEvent?.Dispose();
internal nint LockIdForEvents
{
get
{
- Debug.Assert(_waitEvent != null);
- return _waitEvent.SafeWaitHandle.DangerousGetHandle();
+ Debug.Assert(WaitEvent != null);
+ return WaitEvent.SafeWaitHandle.DangerousGetHandle();
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Monitor.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Monitor.cs
index 0eaf0a79c14e9c..9910b6e8af1afa 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Monitor.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Monitor.cs
@@ -78,17 +78,15 @@ private static void ThrowLockTakenException()
throw new ArgumentException(SR.Argument_MustBeFalse, "lockTaken");
}
- #region Object->Condition mapping
-
- private static readonly ConditionalWeakTable