Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,31 @@ internal sealed class Waiter
[ThreadStatic]
private static Waiter? t_waiterForCurrentThread;

// Takes the cached Waiter for this thread (or allocates a new one) and removes the
// current wait's cached Waiter from the thread-static so that any reentrant
// Monitor.Wait (for example, from a SynchronizationContext message pump) gets its own Waiter with a distinct AutoResetEvent.
private static Waiter GetWaiterForCurrentThread()
{
Waiter waiter = t_waiterForCurrentThread ??= new Waiter();
Waiter? waiter = t_waiterForCurrentThread;
if (waiter is not null)
{
t_waiterForCurrentThread = null;
}
else
{
waiter = new Waiter();
}

waiter.signalled = false;
return waiter;
}

private static void ReleaseWaiterForCurrentThread(Waiter waiter)
{
// Return the waiter to the thread-static cache for reuse.
t_waiterForCurrentThread = waiter;
}
Comment thread
eduardo-vp marked this conversation as resolved.

private readonly Lock _lock;
private Waiter? _waitersHead;
private Waiter? _waitersTail;
Expand Down Expand Up @@ -139,6 +157,7 @@ public unsafe bool Wait(int millisecondsTimeout, object associatedObjectForMonit
}

AssertIsNotInList(waiter);
ReleaseWaiterForCurrentThread(waiter);
}

return waiter.signalled;
Expand Down
65 changes: 65 additions & 0 deletions src/libraries/System.Threading/tests/MonitorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -510,5 +510,70 @@ public static void InterruptWaitTest()
waitForThread();
}
}

// Validates that reentrant Monitor.Wait calls from a SynchronizationContext
// message pump do not steal each other's pulse signals.
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/123173", TestRuntimes.Mono)]
Comment thread
eduardo-vp marked this conversation as resolved.
public static void ReentrantWaitFromSyncContextTest()
{
// Since we set the SynchronizationContext for the current thread, we run
// this test in a background thread to avoid affecting other tests.
ThreadTestHelpers.RunTestInBackgroundThread(() =>
{
object lockA = new();
object lockB = new();
bool innerWaitResult = true; // should become false (lockB is never pulsed)

var pumpCtx = new ReentrantWaitSyncContext(() =>
{
lock (lockB)
{
innerWaitResult = Monitor.Wait(lockB, 500);
}
});
SynchronizationContext.SetSynchronizationContext(pumpCtx);

var t = new Thread(() =>
{
Thread.Sleep(100);
lock (lockA) { Monitor.Pulse(lockA); }
}) { IsBackground = true };
t.Start();

bool outerResult;
var sw = System.Diagnostics.Stopwatch.StartNew();
lock (lockA)
{
Comment thread
eduardo-vp marked this conversation as resolved.
outerResult = Monitor.Wait(lockA, FailTimeoutMilliseconds);
}
long elapsed = sw.ElapsedMilliseconds;

Assert.True(outerResult, "Outer Monitor.Wait should have been pulsed");
Assert.True(elapsed < FailTimeoutMilliseconds / 2,
$"Outer Monitor.Wait took {elapsed}ms — signal was likely stolen by inner wait");
Assert.False(innerWaitResult,
"Inner Monitor.Wait(lockB) should return false (lockB was never pulsed)");

t.Join(FailTimeoutMilliseconds);
Comment thread
eduardo-vp marked this conversation as resolved.
});
}

private sealed class ReentrantWaitSyncContext : SynchronizationContext
{
private Action? _callback;

public ReentrantWaitSyncContext(Action callback)
{
_callback = callback;
SetWaitNotificationRequired();
}

public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
{
Interlocked.Exchange(ref _callback, null)?.Invoke();
return base.Wait(waitHandles, waitAll, millisecondsTimeout);
}
}
}
}
Loading