') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Make Condition to not use ConditionalWeakTable by VSadov · Pull Request #129083 · dotnet/runtime · GitHub
Skip to content

Make Condition to not use ConditionalWeakTable - #129083

Merged
VSadov merged 23 commits into
dotnet:mainfrom
VSadov:cobReg
Jun 12, 2026
Merged

Make Condition to not use ConditionalWeakTable#129083
VSadov merged 23 commits into
dotnet:mainfrom
VSadov:cobReg

Conversation

@VSadov

@VSadovVSadov commented Jun 6, 2026

Copy link
Copy Markdown
Member

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 Consider aging dependent handles the same way as the other kinds of handles. #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.

CopilotAI review requested due to automatic review settings June 6, 2026 22:04
@VSadovVSadov added this to the 11.0.0 milestone Jun 6, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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→Condition table with Lock-based Wait/Pulse/PulseAll helpers.
  • Update Lock to store either an AutoResetEvent or a Condition in a single field and lazily create the Condition when needed.
  • Update ManualResetEventSlim to use Lock and 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
FileDescription
src/libraries/System.Private.CoreLib/src/System/Threading/Monitor.csRoutes Wait/Pulse/PulseAll through the sync-block Lock instead of a global ConditionalWeakTable.
src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.csSwitches internal wait/pulse lock to Lock and rewrites signaled/waiter state manipulation.
src/libraries/System.Private.CoreLib/src/System/Threading/Lock.csIntroduces _waitEventOrCondition union field and adds internal Wait/Pulse/PulseAll via Condition.
src/libraries/System.Private.CoreLib/src/System/Threading/Condition.csRefactors waiter bookkeeping and signaling strategy; adds storage for a Lock’s wait event.
src/coreclr/vm/corelib.hUpdates binder field list to remove Monitor.s_conditionTable and add Lock._waitEventOrCondition.
src/coreclr/debug/daccess/dacdbiimpl.cppChanges monitor-wait enumeration to locate Condition via the sync-block Lock field.

CopilotAI review requested due to automatic review settings June 7, 2026 00:27

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

@VSadov
VSadov marked this pull request as ready for review June 7, 2026 01:23
CopilotAI review requested due to automatic review settings June 7, 2026 01:23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

@VSadov
VSadov requested a review from jkotasJune 7, 2026 04:34
@jkotas

Copy link
Copy Markdown
Member

Is there a micro-benchmark that demonstrates the improvement?

@VSadov

Copy link
Copy Markdown
MemberAuthor

@MihuBot benchmark System.Threading

@MihuBot

Copy link
Copy Markdown
System.Threading.Tests.Perf_Volatile
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-NRQIIJ : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-NGSIDY : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False PowerPlanMode= IterationTime=250ms
MaxIterationCount=20 MinIterationCount=15 WarmupCount=1
Min=1.124 ns
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
Write_doubleMain1.124 ns0.0003 ns1.00-NA
Write_doublePR1.124 ns0.0002 ns1.00-NA
Read_doubleMain1.126 ns0.0039 ns1.00-NA
Read_doublePR1.125 ns0.0003 ns1.00-NA
System.Threading.Tests.Perf_Timer
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-NRQIIJ : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-NGSIDY : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False PowerPlanMode= IterationTime=250ms
MaxIterationCount=20 MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
ShortScheduleAndDisposeMain91.70 ns0.977 ns1.00120 B1.00
ShortScheduleAndDisposePR82.42 ns0.308 ns0.90120 B1.00
LongScheduleAndDisposeMain82.53 ns0.976 ns1.00120 B1.00
LongScheduleAndDisposePR83.98 ns0.829 ns1.02120 B1.00
ScheduleManyThenDisposeManyMain243,251,019.53 ns2,833,356.669 ns1.00144001288 B1.00
ScheduleManyThenDisposeManyPR242,188,481.40 ns2,484,566.988 ns1.00144001288 B1.00
ShortScheduleAndDisposeWithFiringTimersMain97.65 ns3.355 ns1.00144 B1.00
ShortScheduleAndDisposeWithFiringTimersPR92.94 ns2.533 ns0.95144 B1.00
SynchronousContentionMain1,531,794,458.79 ns31,596,773.598 ns1.001152000776 B1.00
SynchronousContentionPR1,323,815,852.74 ns48,389,218.139 ns0.861152000760 B1.00
AsynchronousContentionMain1,143,753,328.80 ns13,338,983.380 ns1.001344002248 B1.00
AsynchronousContentionPR1,375,212,284.55 ns33,176,643.831 ns1.201344002232 B1.00
System.Threading.Tests.Perf_ThreadStatic
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
GetThreadStaticMain2.489 ns0.0008 ns1.00-NA
GetThreadStaticPR2.490 ns0.0019 ns1.00-NA
SetThreadStaticMain4.397 ns0.0026 ns1.00-NA
SetThreadStaticPR4.400 ns0.0019 ns1.00-NA
System.Threading.Tests.Perf_ThreadPool
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1 StdDev=0.0297 s
Gen0=38000.0000
MethodToolchainWorkItemsPerCoreMeanErrorRatioAllocatedAlloc Ratio
QueueUserWorkItem_WaitCallback_ThroughputMain200000002.119 s0.0318 s1.00610.35 MB1.00
QueueUserWorkItem_WaitCallback_ThroughputPR200000002.115 s0.0317 s1.00610.35 MB1.00
System.Threading.Tests.Perf_Thread
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-NRQIIJ : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-NGSIDY : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False PowerPlanMode= IterationTime=250ms
MaxIterationCount=20 MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
CurrentThreadMain2.760 ns0.0010 ns1.00-NA
CurrentThreadPR2.761 ns0.0011 ns1.00-NA
GetCurrentProcessorIdMain3.036 ns0.0029 ns1.00-NA
GetCurrentProcessorIdPR3.060 ns0.0297 ns1.01-NA
System.Threading.Tests.Perf_SpinLock
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
EnterExitMain4.409 ns0.0026 ns1.00-NA
EnterExitPR4.413 ns0.0044 ns1.00-NA
TryEnterExitMain4.411 ns0.0045 ns1.00-NA
TryEnterExitPR4.409 ns0.0017 ns1.00-NA
TryEnter_FailMain1.910 ns0.0011 ns1.00-NA
TryEnter_FailPR1.913 ns0.0042 ns1.00-NA
System.Threading.Tests.Perf_SemaphoreSlim
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
ReleaseWaitMain21.51 ns0.036 ns1.00-NA
ReleaseWaitPR21.54 ns0.157 ns1.00-NA
ReleaseWaitAsyncMain21.04 ns0.027 ns1.00-NA
ReleaseWaitAsyncPR20.85 ns0.055 ns0.99-NA
ReleaseWaitAsync_WithCancellationTokenMain18,771.65 ns1,508.351 ns1.00584 B1.00
ReleaseWaitAsync_WithCancellationTokenPR19,041.39 ns1,888.110 ns1.02584 B1.00
ReleaseWaitAsync_WithTimeoutMain18,714.22 ns1,490.603 ns1.00680 B1.00
ReleaseWaitAsync_WithTimeoutPR17,459.44 ns874.799 ns0.94680 B1.00
ReleaseWaitAsync_WithCancellationTokenAndTimeoutMain19,060.74 ns911.619 ns1.00680 B1.00
ReleaseWaitAsync_WithCancellationTokenAndTimeoutPR18,601.40 ns1,528.630 ns0.98680 B1.00
System.Threading.Tests.Perf_Monitor
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
EnterExitMain8.628 ns0.1177 ns1.00-NA
EnterExitPR10.146 ns0.0306 ns1.18-NA
TryEnterExitMain8.699 ns0.1159 ns1.00-NA
TryEnterExitPR8.762 ns0.0342 ns1.01-NA
System.Threading.Tests.Perf_Lock
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1 StdDev=0.012 ns
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
ReaderWriterLockSlimPerfMain12.20 ns0.013 ns1.00-NA
ReaderWriterLockSlimPerfPR12.17 ns0.014 ns1.00-NA
System.Threading.Tests.Perf_Interlocked
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
Increment_intMain2.108 ns0.0009 ns1.00-NA
Increment_intPR2.107 ns0.0009 ns1.00-NA
Decrement_intMain2.109 ns0.0009 ns1.00-NA
Decrement_intPR2.106 ns0.0010 ns1.00-NA
Increment_longMain2.108 ns0.0013 ns1.00-NA
Increment_longPR2.107 ns0.0006 ns1.00-NA
Decrement_longMain2.108 ns0.0012 ns1.00-NA
Decrement_longPR2.108 ns0.0012 ns1.00-NA
Add_intMain2.108 ns0.0010 ns1.00-NA
Add_intPR2.108 ns0.0008 ns1.00-NA
Add_longMain2.108 ns0.0011 ns1.00-NA
Add_longPR2.107 ns0.0011 ns1.00-NA
Exchange_intMain2.185 ns0.0006 ns1.00-NA
Exchange_intPR2.185 ns0.0006 ns1.00-NA
Exchange_longMain2.185 ns0.0007 ns1.00-NA
Exchange_longPR2.186 ns0.0008 ns1.00-NA
CompareExchange_intMain2.394 ns0.0007 ns1.00-NA
CompareExchange_intPR2.394 ns0.0006 ns1.00-NA
CompareExchange_longMain2.394 ns0.0007 ns1.00-NA
CompareExchange_longPR2.395 ns0.0011 ns1.00-NA
CompareExchange_object_MatchMain2.489 ns0.0014 ns1.00-NA
CompareExchange_object_MatchPR2.395 ns0.0015 ns0.96-NA
CompareExchange_object_NoMatchMain2.489 ns0.0008 ns1.00-NA
CompareExchange_object_NoMatchPR2.398 ns0.0012 ns0.96-NA
System.Threading.Tests.Perf_EventWaitHandle
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1 Min=19.45 ns
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
Set_ResetMain19.48 ns0.025 ns1.00-NA
Set_ResetPR19.49 ns0.021 ns1.00-NA
System.Threading.Tests.Perf_CancellationToken
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-NRQIIJ : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-NGSIDY : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False PowerPlanMode= IterationTime=250ms
MaxIterationCount=20 MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
RegisterAndUnregister_SerialMain24.138 ns0.1453 ns1.00-NA
RegisterAndUnregister_SerialPR23.736 ns0.3858 ns0.98-NA
CancelMain48.256 ns0.4052 ns1.00192 B1.00
CancelPR48.049 ns0.3128 ns1.00192 B1.00
CreateLinkedTokenSource1Main27.951 ns0.3240 ns1.0064 B1.00
CreateLinkedTokenSource1PR28.637 ns0.5653 ns1.0264 B1.00
CreateLinkedTokenSource2Main46.560 ns0.7770 ns1.0080 B1.00
CreateLinkedTokenSource2PR44.870 ns0.4932 ns0.9680 B1.00
CreateLinkedTokenSource3Main73.413 ns1.4567 ns1.00128 B1.00
CreateLinkedTokenSource3PR72.403 ns1.4278 ns0.99128 B1.00
CreateTokenDisposeMain7.761 ns0.0516 ns1.0048 B1.00
CreateTokenDisposePR7.763 ns0.0186 ns1.0048 B1.00
CreateRegisterDisposeMain43.158 ns0.4251 ns1.00192 B1.00
CreateRegisterDisposePR41.951 ns0.4338 ns0.97192 B1.00
CreateManyRegisterDisposeMain13.192 ns0.2554 ns1.00-NA
CreateManyRegisterDisposePR13.135 ns0.2028 ns1.00-NA
CreateManyRegisterMultipleDisposeMain91.201 ns0.2628 ns1.00-NA
CreateManyRegisterMultipleDisposePR91.477 ns0.1414 ns1.00-NA
CancelAfterMain57.793 ns0.4665 ns1.00144 B1.00
CancelAfterPR58.719 ns0.6143 ns1.02144 B1.00
System.Threading.Tasks.Tests.Perf_AsyncMethods
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
EmptyAsyncMethodInvocationMain5.038 ns0.0074 ns1.00-NA
EmptyAsyncMethodInvocationPR5.028 ns0.0066 ns1.00-NA
SingleYieldMethodInvocationMain143.936 ns0.9650 ns1.00168 B1.00
SingleYieldMethodInvocationPR137.935 ns0.6620 ns0.96168 B1.00
YieldMain76.086 ns0.7959 ns1.0024 B1.00
YieldPR72.520 ns0.6098 ns0.9524 B1.00
System.Threading.Tasks.ValueTaskPerfTest
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-UXTJFQ : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-WCARJH : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-XVCCJK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HMQJNI : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False PowerPlanMode= IterationTime=250ms
MaxIterationCount=20 MaxWarmupIterationCount=10 MinIterationCount=15
MinWarmupIterationCount=2 WarmupCount=-1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
Await_FromResultMain7.622 ns0.0291 ns1.00-NA
Await_FromResultPR7.767 ns0.0137 ns1.02-NA
Await_FromCompletedTaskMain13.296 ns0.0772 ns1.0072 B1.00
Await_FromCompletedTaskPR13.303 ns0.1925 ns1.0072 B1.00
Await_FromCompletedValueTaskSourceMain19.073 ns0.2003 ns1.0072 B1.00
Await_FromCompletedValueTaskSourcePR18.853 ns0.1334 ns0.9972 B1.00
CreateAndAwait_FromResultMain7.686 ns0.0070 ns1.00-NA
CreateAndAwait_FromResultPR7.599 ns0.0045 ns0.99-NA
CreateAndAwait_FromResult_ConfigureAwaitMain7.586 ns0.0059 ns1.00-NA
CreateAndAwait_FromResult_ConfigureAwaitPR7.605 ns0.0115 ns1.00-NA
CreateAndAwait_FromCompletedTaskMain9.341 ns0.0358 ns1.00-NA
CreateAndAwait_FromCompletedTaskPR9.352 ns0.0707 ns1.00-NA
CreateAndAwait_FromCompletedTask_ConfigureAwaitMain10.649 ns0.1858 ns1.00-NA
CreateAndAwait_FromCompletedTask_ConfigureAwaitPR9.387 ns0.1298 ns0.88-NA
CreateAndAwait_FromCompletedValueTaskSourceMain10.947 ns0.0313 ns1.00-NA
CreateAndAwait_FromCompletedValueTaskSourcePR10.938 ns0.0396 ns1.00-NA
CreateAndAwait_FromYieldingAsyncMethodMain246.732 ns1.8634 ns1.00392 B1.00
CreateAndAwait_FromYieldingAsyncMethodPR246.383 ns3.4310 ns1.00392 B1.00
CreateAndAwait_FromDelayedTCSMain18,061.220 ns1,681.9837 ns1.00519 B1.00
CreateAndAwait_FromDelayedTCSPR17,440.193 ns1,339.9493 ns0.98518 B1.00
Copy_PassAsArgumentAndReturn_FromResultMain4.364 ns0.0019 ns1.00-NA
Copy_PassAsArgumentAndReturn_FromResultPR4.642 ns0.0061 ns1.06-NA
Copy_PassAsArgumentAndReturn_FromTaskMain8.424 ns0.1486 ns1.00-NA
Copy_PassAsArgumentAndReturn_FromTaskPR8.728 ns0.0030 ns1.04-NA
Copy_PassAsArgumentAndReturn_FromValueTaskSourceMain11.727 ns0.0076 ns1.00-NA
Copy_PassAsArgumentAndReturn_FromValueTaskSourcePR11.910 ns0.2263 ns1.02-NA
CreateAndAwait_FromCompletedValueTaskSource_ConfigureAwaitMain10.839 ns0.0543 ns1.00-NA
CreateAndAwait_FromCompletedValueTaskSource_ConfigureAwaitPR10.933 ns0.0833 ns1.01-NA
System.Threading.Channels.Tests.UnboundedChannelPerfTests
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
TryWriteThenTryReadMain21.26 ns0.011 ns1.00-NA
TryWriteThenTryReadPR21.27 ns0.034 ns1.00-NA
WriteAsyncThenReadAsyncMain29.23 ns0.076 ns1.00-NA
WriteAsyncThenReadAsyncPR28.29 ns0.025 ns0.97-NA
ReadAsyncThenWriteAsyncMain51.77 ns4.620 ns1.00-NA
ReadAsyncThenWriteAsyncPR45.70 ns0.020 ns0.89-NA
PingPongMain3,017,820.16 ns268,337.188 ns1.001079 B1.00
PingPongPR2,995,050.16 ns170,307.137 ns1.001081 B1.00
System.Threading.Channels.Tests.SpscUnboundedChannelPerfTests
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
TryWriteThenTryReadMain21.90 ns0.085 ns1.00-NA
TryWriteThenTryReadPR21.29 ns0.056 ns0.97-NA
WriteAsyncThenReadAsyncMain34.87 ns0.050 ns1.00-NA
WriteAsyncThenReadAsyncPR34.69 ns0.038 ns0.99-NA
ReadAsyncThenWriteAsyncMain43.22 ns0.027 ns1.00-NA
ReadAsyncThenWriteAsyncPR42.72 ns0.059 ns0.99-NA
PingPongMain2,880,382.89 ns183,139.833 ns1.001079 B1.00
PingPongPR2,926,050.04 ns229,088.081 ns1.021079 B1.00
System.Threading.Channels.Tests.BoundedChannelPerfTests
BenchmarkDotNet v0.16.0-nightly.20260518.1249, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 1.89 GB Available
Job-TPEJOW : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
Job-HKHXHK : .NET 11.0.0 (11.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4
EvaluateOverhead=False OutlierMode=Default PowerPlanMode=
IterationTime=250ms MaxIterationCount=20 MemoryRandomization=Default
MinIterationCount=15 WarmupCount=1
MethodToolchainMeanErrorRatioAllocatedAlloc Ratio
TryWriteThenTryReadMain30.64 ns0.055 ns1.00-NA
TryWriteThenTryReadPR30.86 ns0.015 ns1.01-NA
WriteAsyncThenReadAsyncMain37.06 ns0.040 ns1.00-NA
WriteAsyncThenReadAsyncPR39.90 ns0.070 ns1.08-NA
ReadAsyncThenWriteAsyncMain42.11 ns0.274 ns1.00-NA
ReadAsyncThenWriteAsyncPR42.30 ns0.050 ns1.00-NA
PingPongMain3,260,739.75 ns236,075.870 ns1.001081 B1.00
PingPongPR3,259,202.12 ns244,386.437 ns1.011081 B1.00

@VSadov

Copy link
Copy Markdown
MemberAuthor

@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);}}}

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

@VSadov

Copy link
Copy Markdown
MemberAuthor

@MihuBot benchmark System.Buffers

@MihuBot

Copy link
Copy Markdown

@VSadov

Copy link
Copy Markdown
MemberAuthor

@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

Copy link
Copy Markdown
MemberAuthor

@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();}}}

@VSadov

VSadov commented Jun 11, 2026

Copy link
Copy Markdown
MemberAuthor

Is there a micro-benchmark that demonstrates the improvement?

LLM came out with 3 benchmarks for the impact of this change:

  • ParallelFirstBlockingWait - to measure the contention effects
  • Gen0CollectionsWithManyLiveInstances - effect on GC pause
  • PingPong - effect on throughput of two threads unblocking each other via ManualResetEventSlim

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:

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V45 2.60GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK 11.0.100-preview.6.26310.110
[Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v4
RatioSD=0.02 
MethodToolchainIterationsMeanErrorRatioAllocatedAlloc Ratio
PingPongPR #129083**100015.97 ms0.256 ms1.00424 B1.00
PingPongmain100016.34 ms0.216 ms1.02499 B1.18
PingPongPR #129083**1000001,649.53 ms24.462 ms1.00424 B1.00
PingPongmain1000001,679.25 ms33.129 ms1.02720 B1.70

Full logs

And even with the default spinning, that shields somewhat from the effects of waiting:

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V45 2.60GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK 11.0.100-preview.6.26310.110
[Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v4
MethodToolchainIterationsMeanErrorRatioAllocatedAlloc Ratio
PingPongPR #129083**1000147.1 μs2.86 μs1.00248 B1.00
PingPongmain1000163.3 μs2.42 μs1.11248 B1.00
PingPongPR #129083**10000015,097.8 μs298.71 μs1.00252 B1.00
PingPongmain10000016,255.1 μs324.92 μs1.08251 B1.00

Full logs

@VSadov

VSadov commented Jun 11, 2026

Copy link
Copy Markdown
MemberAuthor

I've also run PingPong with [MemoryDiagnoser] and in the forced no-spinning case the PR version allocates clearly less.
(the first table above)

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Monitor.cs Outdated
@jkotasjkotas added the tenet-performance Performance related issue label Jun 11, 2026
CopilotAI review requested due to automatic review settings June 11, 2026 20:23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

@VSadov
VSadov enabled auto-merge (squash) June 11, 2026 21:37
@VSadov
VSadov merged commit 63e7364 into dotnet:mainJun 12, 2026
157 of 159 checks passed
@VSadov
VSadov deleted the cobReg branch June 12, 2026 01:37
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
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.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@VSadov@jkotas@MihuBot