Uh oh!
There was an error while loading. Please reload this page.
Make Condition to not use ConditionalWeakTable - #129083
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors how Monitor.Wait/Pulse are implemented by removing the ConditionalWeakTable<object, Condition> mapping and instead associating a Condition directly with the managed Lock used by a sync block. It also updates diagnostics code (DAC/DBI) to discover monitor waiters via the Lock rather than via Monitor’s static table, and updates ManualResetEventSlim to use Lock for its wait/pulse coordination.
Changes:
- Replace
Monitor’s object→Conditiontable withLock-basedWait/Pulse/PulseAllhelpers. - Update
Lockto store either anAutoResetEventor aConditionin a single field and lazily create theConditionwhen needed. - Update
ManualResetEventSlimto useLockand revise waiter-count/state handling; update DAC monitor-wait enumeration accordingly.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System/Threading/Monitor.cs | Routes Wait/Pulse/PulseAll through the sync-block Lock instead of a global ConditionalWeakTable. |
| src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs | Switches internal wait/pulse lock to Lock and rewrites signaled/waiter state manipulation. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs | Introduces _waitEventOrCondition union field and adds internal Wait/Pulse/PulseAll via Condition. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Condition.cs | Refactors waiter bookkeeping and signaling strategy; adds storage for a Lock’s wait event. |
| src/coreclr/vm/corelib.h | Updates binder field list to remove Monitor.s_conditionTable and add Lock._waitEventOrCondition. |
| src/coreclr/debug/daccess/dacdbiimpl.cpp | Changes monitor-wait enumeration to locate Condition via the sync-block Lock field. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
jkotas
commented
Jun 7, 2026
Is there a micro-benchmark that demonstrates the improvement? |
VSadov
commented
Jun 10, 2026
@MihuBot benchmark System.Threading |
MihuBot
commented
Jun 10, 2026
System.Threading.Tests.Perf_Volatile
System.Threading.Tests.Perf_Timer
System.Threading.Tests.Perf_ThreadStatic
System.Threading.Tests.Perf_ThreadPool
System.Threading.Tests.Perf_Thread
System.Threading.Tests.Perf_SpinLock
System.Threading.Tests.Perf_SemaphoreSlim
System.Threading.Tests.Perf_Monitor
System.Threading.Tests.Perf_Lock
System.Threading.Tests.Perf_Interlocked
System.Threading.Tests.Perf_EventWaitHandle
System.Threading.Tests.Perf_CancellationToken
System.Threading.Tasks.Tests.Perf_AsyncMethods
System.Threading.Tasks.ValueTaskPerfTest
System.Threading.Channels.Tests.UnboundedChannelPerfTests
System.Threading.Channels.Tests.SpscUnboundedChannelPerfTests
System.Threading.Channels.Tests.BoundedChannelPerfTests
|
VSadov
commented
Jun 10, 2026
@EgorBot -x64 usingBenchmarkDotNet.Attributes;usingSystem.Threading.Tasks;namespaceSystem.Threading.Tests{publicclassPerf_ManualResetEventSlim_SteadyState{[Params(1_000,100_000)]publicintIterations;[Benchmark]publicvoidPingPong(){varmres1=newManualResetEventSlim(false);varmres2=newManualResetEventSlim(false);Taskt=Task.Run(()=>{for(inti=0;i<Iterations;i++){mres1.Wait();mres1.Reset();mres2.Set();}});for(inti=0;i<Iterations;i++){mres1.Set();mres2.Wait();mres2.Reset();}t.Wait();}}publicclassPerf_ManualResetEventSlim_FirstUseContention{[Params(1,4,16,64)]publicintThreads;[Params(1_000)]publicintOperationsPerThread;[Benchmark]publicvoidParallelFirstBlockingWait(){Task[]tasks=newTask[Threads];varstart=newManualResetEventSlim(false);for(intt=0;t<Threads;t++){tasks[t]=Task.Run(()=>{start.Wait();for(inti=0;i<OperationsPerThread;i++){varmres=newManualResetEventSlim(false);Tasksignaler=Task.Run(()=>mres.Set());mres.Wait();signaler.Wait();}});}start.Set();Task.WaitAll(tasks);}}[MemoryDiagnoser]publicclassPerf_ManualResetEventSlim_GC{[Params(10_000,100_000)]publicintCount;privateManualResetEventSlim[]_events;[GlobalSetup]publicvoidSetup(){_events=newManualResetEventSlim[Count];for(inti=0;i<Count;i++){varmres=newManualResetEventSlim(false);Taskt=Task.Run(()=>mres.Set());mres.Wait();t.Wait();mres.Reset();_events[i]=mres;}GC.Collect();GC.WaitForPendingFinalizers();GC.Collect();}[Benchmark]publicvoidGen0CollectionsWithManyLiveInstances(){for(inti=0;i<100;i++){GC.Collect(0,GCCollectionMode.Forced,blocking:true);}GC.KeepAlive(_events);}}} |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
VSadov
commented
Jun 10, 2026
@MihuBot benchmark System.Buffers |
MihuBot
commented
Jun 10, 2026
See benchmark results at https://gist.github.com/MihuBot/63e4f13a5826f3e15a5353d77739f04b |
VSadov
commented
Jun 10, 2026
@EgorBot -x64 usingBenchmarkDotNet.Attributes;usingSystem.Threading.Tasks;namespaceSystem.Threading.Tests{[MemoryDiagnoser]publicclassPerf_ManualResetEventSlim_SteadyState{[Params(1_000,100_000)]publicintIterations;[Benchmark]publicvoidPingPong(){varmres1=newManualResetEventSlim(false,0);varmres2=newManualResetEventSlim(false,0);Taskt=Task.Run(()=>{for(inti=0;i<Iterations;i++){mres1.Wait();mres1.Reset();mres2.Set();}});for(inti=0;i<Iterations;i++){mres1.Set();mres2.Wait();mres2.Reset();}t.Wait();}}} |
VSadov
commented
Jun 10, 2026
@EgorBot -x64 usingBenchmarkDotNet.Attributes;usingSystem.Threading.Tasks;namespaceSystem.Threading.Tests{[MemoryDiagnoser]publicclassPerf_ManualResetEventSlim_SteadyState{[Params(1_000,100_000)]publicintIterations;[Benchmark]publicvoidPingPong(){varmres1=newManualResetEventSlim(false);varmres2=newManualResetEventSlim(false);Taskt=Task.Run(()=>{for(inti=0;i<Iterations;i++){mres1.Wait();mres1.Reset();mres2.Set();}});for(inti=0;i<Iterations;i++){mres1.Set();mres2.Wait();mres2.Reset();}t.Wait();}}} |
LLM came out with 3 benchmarks for the impact of this change:
The first two benchmarks are inconclusive. Possibly we do not test for large enough state or with enough concurrency. The PingPong however shows consistent improvements. with spinning disabled we see:
And even with the default spinning, that shields somewhat from the effects of waiting:
|
I've also run PingPong with |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Main motivation is that `ManualResetEventSlim` uses `Monitor.Wait` to implement `Wait` that is: * cancellable * interruptible (in `Thread.Interrupt` sense) and * aware of synchronization context `Monitor.Wait` is a good fit to implement such pattern and should generally perform well enough. `ManualResetEventSlim` in turn is used in `Task.Wait` and some scenarios can wait on Tasks relatively frequently. In such scenarios using `ConditionalWaitTable` for Lock->Condition association may have two inconveniences: * it may result in quite a few dependent handles being alive and that can have impact on GC. In particular because dependent handles currently do not age and need to be revisited in every Gen0, even if both objects referred from the handle may be Gen2 objects. (we should probably address #79062, regardless of this PR) * Allocating an entry in `ConditionalWaitTable` acquires table-wide lock and on large enough core count may contend. It seems there is a relatively simple way to arrange Lock->Condition link without involving `ConditionalWaitTable`, thus why not. The change also enables `Wait`/`Pulse`/`PulseAll` functionality on `Lock`, but only internally. It can be exposed as a public API, but it would be a separate discussion.
Main motivation is that
ManualResetEventSlimusesMonitor.Waitto implementWaitthat is:Thread.Interruptsense) andMonitor.Waitis a good fit to implement such pattern and should generally perform well enough.ManualResetEventSlimin turn is used inTask.Waitand some scenarios can wait on Tasks relatively frequently.In such scenarios using
ConditionalWaitTablefor Lock->Condition association may have two inconveniences:it may result in quite a few dependent handles being alive and that can have impact on GC.
In particular because dependent handles currently do not age and need to be revisited in every Gen0, even if both objects referred from the handle may be Gen2 objects.
(we should probably address Consider aging dependent handles the same way as the other kinds of handles. #79062, regardless of this PR)
Allocating an entry in
ConditionalWaitTableacquires table-wide lock and on large enough core count may contend.It seems there is a relatively simple way to arrange Lock->Condition link without involving
ConditionalWaitTable, thus why not.The change also enables
Wait/Pulse/PulseAllfunctionality onLock, but only internally.It can be exposed as a public API, but it would be a separate discussion.