There's a current bug that can happen when using synchronization contexts that allow message pumping:
- The main thread calls Monitor.Wait on lock A.
- SynchronizationContext.Wait pumps a message and the main thread calls Monitor.Wait on lock B.
- A background thread calls Monitor.Pulse on lock A but lock B steals the signal.
|
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) |
|
{ |
|
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); |
|
}); |
|
} |
There's a current bug that can happen when using synchronization contexts that allow message pumping:
runtime/src/libraries/System.Threading/tests/MonitorTests.cs
Lines 518 to 560 in d343974