Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState> - #127981

Merged
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback
May 12, 2026
Merged

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState>#127981
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback

Conversation

@jakobbotsch

Copy link
Copy Markdown
Member

When user code forwards continuations passed to
IValueTaskSource.OnCompleted to the thread pool we can avoid allocating a new work item. This function already has a special case that implements this optimization for async1. Add one for runtime async too.

This PR and #127973 closes the RPS gap between async1 and runtime async on ASP.NET platform-json.

…ate>`
When user code forwards continuations passed to
`IValueTaskSource.OnCompleted` to the thread pool we can avoid
allocating a new work item. This function already has a special case
that implements this optimization for async1. Add one for runtime async
too.
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.

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 extends the existing ThreadPool.UnsafeQueueUserWorkItem<TState> fast-path for known async continuation callbacks to also cover runtime-async continuations, enabling user implementations of IValueTaskSource.OnCompleted to forward the provided continuation to the ThreadPool without allocating an extra work-item wrapper.

Changes:

  • Add a new known ThreadPool callback (s_dispatchRuntimeAsyncContinuationsCallback) intended to dispatch runtime-async continuations.
  • Add a corresponding special-case in UnsafeQueueUserWorkItem<TState> to enqueue the Task state directly when that callback is used.
  • Update runtime-async ValueTaskSourceNotifier.OnCompleted wiring to use the new ThreadPool callback, and remove the previous per-type callback.

Reviewed changes

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

FileDescription
src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.csIntroduces the runtime-async known callback and a UnsafeQueueUserWorkItem<TState> special-case to avoid wrapper allocations.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csSwitches runtime-async IValueTaskSource continuation registration to the new ThreadPool callback and deletes the old callback.

@jakobbotsch
jakobbotsch marked this pull request as ready for review May 11, 2026 11:58
CopilotAI review requested due to automatic review settings May 11, 2026 11:58

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs:381

  • The comment says the ThreadPool callback "always passes null for the thread". That’s true for direct invocation of s_dispatchRuntimeAsyncContinuationsCallback, but RuntimeAsyncTask<T> can also be queued as a Task work item (including via the new ThreadPool special-case), in which case ExecuteFromThreadPool will receive a non-null ThreadPool thread. Consider rewording to avoid implying threadPoolThread is always null.
 internal override void ExecuteDirectly(Thread? threadPoolThread)
{
DispatchContinuations();
}

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

@stephentoub Can you take a quick look at this?

Another possibility is that we use the same delegate for both async1 and runtime async here, changing it to something like:

if(stateisTaskt){t.ExecuteDirectly(null);}elseif(stateisIAsyncStateMachineBoxbox){box.MoveNext();}else{ThrowHelper.ThrowUnexpectedStateForKnownCallback(state);}

Then we can avoid the extra check in ThreadPool.UnsafeQueueUserWorkItem<TState>. The normal task/value task continuation path should hit Task t check, but for PoolingAsyncValueTaskMethodBuilder.StateMachineBox I think we still need to check for IAsyncStateMachineBox.
I tried measuring with the benchmark in #55955 and modifying the delegate seems to make it slightly more expensive (1-2%), but honestly I am not sure if that was just variance.

@VSadov

Copy link
Copy Markdown
Member

Another possibility is that we use the same delegate for both async1 and runtime async here

Not every invocation of this delegate will be via thread pool, so I think if we are to trade extra check between enqueue codepath and the delegate itself, it would be very slightly better if it is in the enqueue path (as in this PR - different delegates).

It probably does not matter, but just as a justification to have it on one place or another.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

I'll merge this, but if there's any more feedback please let me know and I can address it separately.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Timeouts

@jakobbotsch
jakobbotsch merged commit 7900769 into dotnet:mainMay 12, 2026
148 of 157 checks passed
@jakobbotsch
jakobbotsch deleted the queue-runtime-async-callback branch May 12, 2026 09:44
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@jakobbotsch@VSadov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState> - #127981

Merged
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback
May 12, 2026
Merged

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState>#127981
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback

Conversation

@jakobbotsch

Copy link
Copy Markdown
Member

When user code forwards continuations passed to
IValueTaskSource.OnCompleted to the thread pool we can avoid allocating a new work item. This function already has a special case that implements this optimization for async1. Add one for runtime async too.

This PR and #127973 closes the RPS gap between async1 and runtime async on ASP.NET platform-json.

…ate>`
When user code forwards continuations passed to
`IValueTaskSource.OnCompleted` to the thread pool we can avoid
allocating a new work item. This function already has a special case
that implements this optimization for async1. Add one for runtime async
too.
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.

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 extends the existing ThreadPool.UnsafeQueueUserWorkItem<TState> fast-path for known async continuation callbacks to also cover runtime-async continuations, enabling user implementations of IValueTaskSource.OnCompleted to forward the provided continuation to the ThreadPool without allocating an extra work-item wrapper.

Changes:

  • Add a new known ThreadPool callback (s_dispatchRuntimeAsyncContinuationsCallback) intended to dispatch runtime-async continuations.
  • Add a corresponding special-case in UnsafeQueueUserWorkItem<TState> to enqueue the Task state directly when that callback is used.
  • Update runtime-async ValueTaskSourceNotifier.OnCompleted wiring to use the new ThreadPool callback, and remove the previous per-type callback.

Reviewed changes

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

FileDescription
src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.csIntroduces the runtime-async known callback and a UnsafeQueueUserWorkItem<TState> special-case to avoid wrapper allocations.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csSwitches runtime-async IValueTaskSource continuation registration to the new ThreadPool callback and deletes the old callback.

@jakobbotsch
jakobbotsch marked this pull request as ready for review May 11, 2026 11:58
CopilotAI review requested due to automatic review settings May 11, 2026 11:58

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs:381

  • The comment says the ThreadPool callback "always passes null for the thread". That’s true for direct invocation of s_dispatchRuntimeAsyncContinuationsCallback, but RuntimeAsyncTask<T> can also be queued as a Task work item (including via the new ThreadPool special-case), in which case ExecuteFromThreadPool will receive a non-null ThreadPool thread. Consider rewording to avoid implying threadPoolThread is always null.
 internal override void ExecuteDirectly(Thread? threadPoolThread)
{
DispatchContinuations();
}

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

@stephentoub Can you take a quick look at this?

Another possibility is that we use the same delegate for both async1 and runtime async here, changing it to something like:

if(stateisTaskt){t.ExecuteDirectly(null);}elseif(stateisIAsyncStateMachineBoxbox){box.MoveNext();}else{ThrowHelper.ThrowUnexpectedStateForKnownCallback(state);}

Then we can avoid the extra check in ThreadPool.UnsafeQueueUserWorkItem<TState>. The normal task/value task continuation path should hit Task t check, but for PoolingAsyncValueTaskMethodBuilder.StateMachineBox I think we still need to check for IAsyncStateMachineBox.
I tried measuring with the benchmark in #55955 and modifying the delegate seems to make it slightly more expensive (1-2%), but honestly I am not sure if that was just variance.

@VSadov

Copy link
Copy Markdown
Member

Another possibility is that we use the same delegate for both async1 and runtime async here

Not every invocation of this delegate will be via thread pool, so I think if we are to trade extra check between enqueue codepath and the delegate itself, it would be very slightly better if it is in the enqueue path (as in this PR - different delegates).

It probably does not matter, but just as a justification to have it on one place or another.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

I'll merge this, but if there's any more feedback please let me know and I can address it separately.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Timeouts

@jakobbotsch
jakobbotsch merged commit 7900769 into dotnet:mainMay 12, 2026
148 of 157 checks passed
@jakobbotsch
jakobbotsch deleted the queue-runtime-async-callback branch May 12, 2026 09:44
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@jakobbotsch@VSadov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState> - #127981

Merged
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback
May 12, 2026
Merged

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState>#127981
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback

Conversation

@jakobbotsch

Copy link
Copy Markdown
Member

When user code forwards continuations passed to
IValueTaskSource.OnCompleted to the thread pool we can avoid allocating a new work item. This function already has a special case that implements this optimization for async1. Add one for runtime async too.

This PR and #127973 closes the RPS gap between async1 and runtime async on ASP.NET platform-json.

…ate>`
When user code forwards continuations passed to
`IValueTaskSource.OnCompleted` to the thread pool we can avoid
allocating a new work item. This function already has a special case
that implements this optimization for async1. Add one for runtime async
too.
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.

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 extends the existing ThreadPool.UnsafeQueueUserWorkItem<TState> fast-path for known async continuation callbacks to also cover runtime-async continuations, enabling user implementations of IValueTaskSource.OnCompleted to forward the provided continuation to the ThreadPool without allocating an extra work-item wrapper.

Changes:

  • Add a new known ThreadPool callback (s_dispatchRuntimeAsyncContinuationsCallback) intended to dispatch runtime-async continuations.
  • Add a corresponding special-case in UnsafeQueueUserWorkItem<TState> to enqueue the Task state directly when that callback is used.
  • Update runtime-async ValueTaskSourceNotifier.OnCompleted wiring to use the new ThreadPool callback, and remove the previous per-type callback.

Reviewed changes

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

FileDescription
src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.csIntroduces the runtime-async known callback and a UnsafeQueueUserWorkItem<TState> special-case to avoid wrapper allocations.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csSwitches runtime-async IValueTaskSource continuation registration to the new ThreadPool callback and deletes the old callback.

@jakobbotsch
jakobbotsch marked this pull request as ready for review May 11, 2026 11:58
CopilotAI review requested due to automatic review settings May 11, 2026 11:58

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs:381

  • The comment says the ThreadPool callback "always passes null for the thread". That’s true for direct invocation of s_dispatchRuntimeAsyncContinuationsCallback, but RuntimeAsyncTask<T> can also be queued as a Task work item (including via the new ThreadPool special-case), in which case ExecuteFromThreadPool will receive a non-null ThreadPool thread. Consider rewording to avoid implying threadPoolThread is always null.
 internal override void ExecuteDirectly(Thread? threadPoolThread)
{
DispatchContinuations();
}

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

@stephentoub Can you take a quick look at this?

Another possibility is that we use the same delegate for both async1 and runtime async here, changing it to something like:

if(stateisTaskt){t.ExecuteDirectly(null);}elseif(stateisIAsyncStateMachineBoxbox){box.MoveNext();}else{ThrowHelper.ThrowUnexpectedStateForKnownCallback(state);}

Then we can avoid the extra check in ThreadPool.UnsafeQueueUserWorkItem<TState>. The normal task/value task continuation path should hit Task t check, but for PoolingAsyncValueTaskMethodBuilder.StateMachineBox I think we still need to check for IAsyncStateMachineBox.
I tried measuring with the benchmark in #55955 and modifying the delegate seems to make it slightly more expensive (1-2%), but honestly I am not sure if that was just variance.

@VSadov

Copy link
Copy Markdown
Member

Another possibility is that we use the same delegate for both async1 and runtime async here

Not every invocation of this delegate will be via thread pool, so I think if we are to trade extra check between enqueue codepath and the delegate itself, it would be very slightly better if it is in the enqueue path (as in this PR - different delegates).

It probably does not matter, but just as a justification to have it on one place or another.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

I'll merge this, but if there's any more feedback please let me know and I can address it separately.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Timeouts

@jakobbotsch
jakobbotsch merged commit 7900769 into dotnet:mainMay 12, 2026
148 of 157 checks passed
@jakobbotsch
jakobbotsch deleted the queue-runtime-async-callback branch May 12, 2026 09:44
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@jakobbotsch@VSadov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState> - #127981

Merged
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback
May 12, 2026
Merged

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState>#127981
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback

Conversation

@jakobbotsch

Copy link
Copy Markdown
Member

When user code forwards continuations passed to
IValueTaskSource.OnCompleted to the thread pool we can avoid allocating a new work item. This function already has a special case that implements this optimization for async1. Add one for runtime async too.

This PR and #127973 closes the RPS gap between async1 and runtime async on ASP.NET platform-json.

…ate>`
When user code forwards continuations passed to
`IValueTaskSource.OnCompleted` to the thread pool we can avoid
allocating a new work item. This function already has a special case
that implements this optimization for async1. Add one for runtime async
too.
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.

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 extends the existing ThreadPool.UnsafeQueueUserWorkItem<TState> fast-path for known async continuation callbacks to also cover runtime-async continuations, enabling user implementations of IValueTaskSource.OnCompleted to forward the provided continuation to the ThreadPool without allocating an extra work-item wrapper.

Changes:

  • Add a new known ThreadPool callback (s_dispatchRuntimeAsyncContinuationsCallback) intended to dispatch runtime-async continuations.
  • Add a corresponding special-case in UnsafeQueueUserWorkItem<TState> to enqueue the Task state directly when that callback is used.
  • Update runtime-async ValueTaskSourceNotifier.OnCompleted wiring to use the new ThreadPool callback, and remove the previous per-type callback.

Reviewed changes

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

FileDescription
src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.csIntroduces the runtime-async known callback and a UnsafeQueueUserWorkItem<TState> special-case to avoid wrapper allocations.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csSwitches runtime-async IValueTaskSource continuation registration to the new ThreadPool callback and deletes the old callback.

@jakobbotsch
jakobbotsch marked this pull request as ready for review May 11, 2026 11:58
CopilotAI review requested due to automatic review settings May 11, 2026 11:58

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs:381

  • The comment says the ThreadPool callback "always passes null for the thread". That’s true for direct invocation of s_dispatchRuntimeAsyncContinuationsCallback, but RuntimeAsyncTask<T> can also be queued as a Task work item (including via the new ThreadPool special-case), in which case ExecuteFromThreadPool will receive a non-null ThreadPool thread. Consider rewording to avoid implying threadPoolThread is always null.
 internal override void ExecuteDirectly(Thread? threadPoolThread)
{
DispatchContinuations();
}

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

@stephentoub Can you take a quick look at this?

Another possibility is that we use the same delegate for both async1 and runtime async here, changing it to something like:

if(stateisTaskt){t.ExecuteDirectly(null);}elseif(stateisIAsyncStateMachineBoxbox){box.MoveNext();}else{ThrowHelper.ThrowUnexpectedStateForKnownCallback(state);}

Then we can avoid the extra check in ThreadPool.UnsafeQueueUserWorkItem<TState>. The normal task/value task continuation path should hit Task t check, but for PoolingAsyncValueTaskMethodBuilder.StateMachineBox I think we still need to check for IAsyncStateMachineBox.
I tried measuring with the benchmark in #55955 and modifying the delegate seems to make it slightly more expensive (1-2%), but honestly I am not sure if that was just variance.

@VSadov

Copy link
Copy Markdown
Member

Another possibility is that we use the same delegate for both async1 and runtime async here

Not every invocation of this delegate will be via thread pool, so I think if we are to trade extra check between enqueue codepath and the delegate itself, it would be very slightly better if it is in the enqueue path (as in this PR - different delegates).

It probably does not matter, but just as a justification to have it on one place or another.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

I'll merge this, but if there's any more feedback please let me know and I can address it separately.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Timeouts

@jakobbotsch
jakobbotsch merged commit 7900769 into dotnet:mainMay 12, 2026
148 of 157 checks passed
@jakobbotsch
jakobbotsch deleted the queue-runtime-async-callback branch May 12, 2026 09:44
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@jakobbotsch@VSadov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState> - #127981

Merged
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback
May 12, 2026
Merged

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState>#127981
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback

Conversation

@jakobbotsch

Copy link
Copy Markdown
Member

When user code forwards continuations passed to
IValueTaskSource.OnCompleted to the thread pool we can avoid allocating a new work item. This function already has a special case that implements this optimization for async1. Add one for runtime async too.

This PR and #127973 closes the RPS gap between async1 and runtime async on ASP.NET platform-json.

…ate>`
When user code forwards continuations passed to
`IValueTaskSource.OnCompleted` to the thread pool we can avoid
allocating a new work item. This function already has a special case
that implements this optimization for async1. Add one for runtime async
too.
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.

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 extends the existing ThreadPool.UnsafeQueueUserWorkItem<TState> fast-path for known async continuation callbacks to also cover runtime-async continuations, enabling user implementations of IValueTaskSource.OnCompleted to forward the provided continuation to the ThreadPool without allocating an extra work-item wrapper.

Changes:

  • Add a new known ThreadPool callback (s_dispatchRuntimeAsyncContinuationsCallback) intended to dispatch runtime-async continuations.
  • Add a corresponding special-case in UnsafeQueueUserWorkItem<TState> to enqueue the Task state directly when that callback is used.
  • Update runtime-async ValueTaskSourceNotifier.OnCompleted wiring to use the new ThreadPool callback, and remove the previous per-type callback.

Reviewed changes

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

FileDescription
src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.csIntroduces the runtime-async known callback and a UnsafeQueueUserWorkItem<TState> special-case to avoid wrapper allocations.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csSwitches runtime-async IValueTaskSource continuation registration to the new ThreadPool callback and deletes the old callback.

@jakobbotsch
jakobbotsch marked this pull request as ready for review May 11, 2026 11:58
CopilotAI review requested due to automatic review settings May 11, 2026 11:58

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs:381

  • The comment says the ThreadPool callback "always passes null for the thread". That’s true for direct invocation of s_dispatchRuntimeAsyncContinuationsCallback, but RuntimeAsyncTask<T> can also be queued as a Task work item (including via the new ThreadPool special-case), in which case ExecuteFromThreadPool will receive a non-null ThreadPool thread. Consider rewording to avoid implying threadPoolThread is always null.
 internal override void ExecuteDirectly(Thread? threadPoolThread)
{
DispatchContinuations();
}

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

@stephentoub Can you take a quick look at this?

Another possibility is that we use the same delegate for both async1 and runtime async here, changing it to something like:

if(stateisTaskt){t.ExecuteDirectly(null);}elseif(stateisIAsyncStateMachineBoxbox){box.MoveNext();}else{ThrowHelper.ThrowUnexpectedStateForKnownCallback(state);}

Then we can avoid the extra check in ThreadPool.UnsafeQueueUserWorkItem<TState>. The normal task/value task continuation path should hit Task t check, but for PoolingAsyncValueTaskMethodBuilder.StateMachineBox I think we still need to check for IAsyncStateMachineBox.
I tried measuring with the benchmark in #55955 and modifying the delegate seems to make it slightly more expensive (1-2%), but honestly I am not sure if that was just variance.

@VSadov

Copy link
Copy Markdown
Member

Another possibility is that we use the same delegate for both async1 and runtime async here

Not every invocation of this delegate will be via thread pool, so I think if we are to trade extra check between enqueue codepath and the delegate itself, it would be very slightly better if it is in the enqueue path (as in this PR - different delegates).

It probably does not matter, but just as a justification to have it on one place or another.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

I'll merge this, but if there's any more feedback please let me know and I can address it separately.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Timeouts

@jakobbotsch
jakobbotsch merged commit 7900769 into dotnet:mainMay 12, 2026
148 of 157 checks passed
@jakobbotsch
jakobbotsch deleted the queue-runtime-async-callback branch May 12, 2026 09:44
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@jakobbotsch@VSadov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState> - #127981

Merged
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback
May 12, 2026
Merged

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState>#127981
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback

Conversation

@jakobbotsch

Copy link
Copy Markdown
Member

When user code forwards continuations passed to
IValueTaskSource.OnCompleted to the thread pool we can avoid allocating a new work item. This function already has a special case that implements this optimization for async1. Add one for runtime async too.

This PR and #127973 closes the RPS gap between async1 and runtime async on ASP.NET platform-json.

…ate>`
When user code forwards continuations passed to
`IValueTaskSource.OnCompleted` to the thread pool we can avoid
allocating a new work item. This function already has a special case
that implements this optimization for async1. Add one for runtime async
too.
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.

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 extends the existing ThreadPool.UnsafeQueueUserWorkItem<TState> fast-path for known async continuation callbacks to also cover runtime-async continuations, enabling user implementations of IValueTaskSource.OnCompleted to forward the provided continuation to the ThreadPool without allocating an extra work-item wrapper.

Changes:

  • Add a new known ThreadPool callback (s_dispatchRuntimeAsyncContinuationsCallback) intended to dispatch runtime-async continuations.
  • Add a corresponding special-case in UnsafeQueueUserWorkItem<TState> to enqueue the Task state directly when that callback is used.
  • Update runtime-async ValueTaskSourceNotifier.OnCompleted wiring to use the new ThreadPool callback, and remove the previous per-type callback.

Reviewed changes

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

FileDescription
src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.csIntroduces the runtime-async known callback and a UnsafeQueueUserWorkItem<TState> special-case to avoid wrapper allocations.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csSwitches runtime-async IValueTaskSource continuation registration to the new ThreadPool callback and deletes the old callback.

@jakobbotsch
jakobbotsch marked this pull request as ready for review May 11, 2026 11:58
CopilotAI review requested due to automatic review settings May 11, 2026 11:58

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs:381

  • The comment says the ThreadPool callback "always passes null for the thread". That’s true for direct invocation of s_dispatchRuntimeAsyncContinuationsCallback, but RuntimeAsyncTask<T> can also be queued as a Task work item (including via the new ThreadPool special-case), in which case ExecuteFromThreadPool will receive a non-null ThreadPool thread. Consider rewording to avoid implying threadPoolThread is always null.
 internal override void ExecuteDirectly(Thread? threadPoolThread)
{
DispatchContinuations();
}

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

@stephentoub Can you take a quick look at this?

Another possibility is that we use the same delegate for both async1 and runtime async here, changing it to something like:

if(stateisTaskt){t.ExecuteDirectly(null);}elseif(stateisIAsyncStateMachineBoxbox){box.MoveNext();}else{ThrowHelper.ThrowUnexpectedStateForKnownCallback(state);}

Then we can avoid the extra check in ThreadPool.UnsafeQueueUserWorkItem<TState>. The normal task/value task continuation path should hit Task t check, but for PoolingAsyncValueTaskMethodBuilder.StateMachineBox I think we still need to check for IAsyncStateMachineBox.
I tried measuring with the benchmark in #55955 and modifying the delegate seems to make it slightly more expensive (1-2%), but honestly I am not sure if that was just variance.

@VSadov

Copy link
Copy Markdown
Member

Another possibility is that we use the same delegate for both async1 and runtime async here

Not every invocation of this delegate will be via thread pool, so I think if we are to trade extra check between enqueue codepath and the delegate itself, it would be very slightly better if it is in the enqueue path (as in this PR - different delegates).

It probably does not matter, but just as a justification to have it on one place or another.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

I'll merge this, but if there's any more feedback please let me know and I can address it separately.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Timeouts

@jakobbotsch
jakobbotsch merged commit 7900769 into dotnet:mainMay 12, 2026
148 of 157 checks passed
@jakobbotsch
jakobbotsch deleted the queue-runtime-async-callback branch May 12, 2026 09:44
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@jakobbotsch@VSadov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState> - #127981

Merged
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback
May 12, 2026
Merged

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState>#127981
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback

Conversation

@jakobbotsch

Copy link
Copy Markdown
Member

When user code forwards continuations passed to
IValueTaskSource.OnCompleted to the thread pool we can avoid allocating a new work item. This function already has a special case that implements this optimization for async1. Add one for runtime async too.

This PR and #127973 closes the RPS gap between async1 and runtime async on ASP.NET platform-json.

…ate>`
When user code forwards continuations passed to
`IValueTaskSource.OnCompleted` to the thread pool we can avoid
allocating a new work item. This function already has a special case
that implements this optimization for async1. Add one for runtime async
too.
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.

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 extends the existing ThreadPool.UnsafeQueueUserWorkItem<TState> fast-path for known async continuation callbacks to also cover runtime-async continuations, enabling user implementations of IValueTaskSource.OnCompleted to forward the provided continuation to the ThreadPool without allocating an extra work-item wrapper.

Changes:

  • Add a new known ThreadPool callback (s_dispatchRuntimeAsyncContinuationsCallback) intended to dispatch runtime-async continuations.
  • Add a corresponding special-case in UnsafeQueueUserWorkItem<TState> to enqueue the Task state directly when that callback is used.
  • Update runtime-async ValueTaskSourceNotifier.OnCompleted wiring to use the new ThreadPool callback, and remove the previous per-type callback.

Reviewed changes

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

FileDescription
src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.csIntroduces the runtime-async known callback and a UnsafeQueueUserWorkItem<TState> special-case to avoid wrapper allocations.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csSwitches runtime-async IValueTaskSource continuation registration to the new ThreadPool callback and deletes the old callback.

@jakobbotsch
jakobbotsch marked this pull request as ready for review May 11, 2026 11:58
CopilotAI review requested due to automatic review settings May 11, 2026 11:58

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs:381

  • The comment says the ThreadPool callback "always passes null for the thread". That’s true for direct invocation of s_dispatchRuntimeAsyncContinuationsCallback, but RuntimeAsyncTask<T> can also be queued as a Task work item (including via the new ThreadPool special-case), in which case ExecuteFromThreadPool will receive a non-null ThreadPool thread. Consider rewording to avoid implying threadPoolThread is always null.
 internal override void ExecuteDirectly(Thread? threadPoolThread)
{
DispatchContinuations();
}

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

@stephentoub Can you take a quick look at this?

Another possibility is that we use the same delegate for both async1 and runtime async here, changing it to something like:

if(stateisTaskt){t.ExecuteDirectly(null);}elseif(stateisIAsyncStateMachineBoxbox){box.MoveNext();}else{ThrowHelper.ThrowUnexpectedStateForKnownCallback(state);}

Then we can avoid the extra check in ThreadPool.UnsafeQueueUserWorkItem<TState>. The normal task/value task continuation path should hit Task t check, but for PoolingAsyncValueTaskMethodBuilder.StateMachineBox I think we still need to check for IAsyncStateMachineBox.
I tried measuring with the benchmark in #55955 and modifying the delegate seems to make it slightly more expensive (1-2%), but honestly I am not sure if that was just variance.

@VSadov

Copy link
Copy Markdown
Member

Another possibility is that we use the same delegate for both async1 and runtime async here

Not every invocation of this delegate will be via thread pool, so I think if we are to trade extra check between enqueue codepath and the delegate itself, it would be very slightly better if it is in the enqueue path (as in this PR - different delegates).

It probably does not matter, but just as a justification to have it on one place or another.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

I'll merge this, but if there's any more feedback please let me know and I can address it separately.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Timeouts

@jakobbotsch
jakobbotsch merged commit 7900769 into dotnet:mainMay 12, 2026
148 of 157 checks passed
@jakobbotsch
jakobbotsch deleted the queue-runtime-async-callback branch May 12, 2026 09:44
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@jakobbotsch@VSadov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState> - #127981

Merged
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback
May 12, 2026
Merged

Special case runtime async in ThreadPool.UnsafeQueueUserWorkItem<TState>#127981
jakobbotsch merged 6 commits into
dotnet:mainfrom
jakobbotsch:queue-runtime-async-callback

Conversation

@jakobbotsch

Copy link
Copy Markdown
Member

When user code forwards continuations passed to
IValueTaskSource.OnCompleted to the thread pool we can avoid allocating a new work item. This function already has a special case that implements this optimization for async1. Add one for runtime async too.

This PR and #127973 closes the RPS gap between async1 and runtime async on ASP.NET platform-json.

…ate>`
When user code forwards continuations passed to
`IValueTaskSource.OnCompleted` to the thread pool we can avoid
allocating a new work item. This function already has a special case
that implements this optimization for async1. Add one for runtime async
too.
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.

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 extends the existing ThreadPool.UnsafeQueueUserWorkItem<TState> fast-path for known async continuation callbacks to also cover runtime-async continuations, enabling user implementations of IValueTaskSource.OnCompleted to forward the provided continuation to the ThreadPool without allocating an extra work-item wrapper.

Changes:

  • Add a new known ThreadPool callback (s_dispatchRuntimeAsyncContinuationsCallback) intended to dispatch runtime-async continuations.
  • Add a corresponding special-case in UnsafeQueueUserWorkItem<TState> to enqueue the Task state directly when that callback is used.
  • Update runtime-async ValueTaskSourceNotifier.OnCompleted wiring to use the new ThreadPool callback, and remove the previous per-type callback.

Reviewed changes

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

FileDescription
src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.csIntroduces the runtime-async known callback and a UnsafeQueueUserWorkItem<TState> special-case to avoid wrapper allocations.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csSwitches runtime-async IValueTaskSource continuation registration to the new ThreadPool callback and deletes the old callback.

@jakobbotsch
jakobbotsch marked this pull request as ready for review May 11, 2026 11:58
CopilotAI review requested due to automatic review settings May 11, 2026 11:58

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs:381

  • The comment says the ThreadPool callback "always passes null for the thread". That’s true for direct invocation of s_dispatchRuntimeAsyncContinuationsCallback, but RuntimeAsyncTask<T> can also be queued as a Task work item (including via the new ThreadPool special-case), in which case ExecuteFromThreadPool will receive a non-null ThreadPool thread. Consider rewording to avoid implying threadPoolThread is always null.
 internal override void ExecuteDirectly(Thread? threadPoolThread)
{
DispatchContinuations();
}

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

@stephentoub Can you take a quick look at this?

Another possibility is that we use the same delegate for both async1 and runtime async here, changing it to something like:

if(stateisTaskt){t.ExecuteDirectly(null);}elseif(stateisIAsyncStateMachineBoxbox){box.MoveNext();}else{ThrowHelper.ThrowUnexpectedStateForKnownCallback(state);}

Then we can avoid the extra check in ThreadPool.UnsafeQueueUserWorkItem<TState>. The normal task/value task continuation path should hit Task t check, but for PoolingAsyncValueTaskMethodBuilder.StateMachineBox I think we still need to check for IAsyncStateMachineBox.
I tried measuring with the benchmark in #55955 and modifying the delegate seems to make it slightly more expensive (1-2%), but honestly I am not sure if that was just variance.

@VSadov

Copy link
Copy Markdown
Member

Another possibility is that we use the same delegate for both async1 and runtime async here

Not every invocation of this delegate will be via thread pool, so I think if we are to trade extra check between enqueue codepath and the delegate itself, it would be very slightly better if it is in the enqueue path (as in this PR - different delegates).

It probably does not matter, but just as a justification to have it on one place or another.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

I'll merge this, but if there's any more feedback please let me know and I can address it separately.

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Timeouts

@jakobbotsch
jakobbotsch merged commit 7900769 into dotnet:mainMay 12, 2026
148 of 157 checks passed
@jakobbotsch
jakobbotsch deleted the queue-runtime-async-callback branch May 12, 2026 09:44
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@jakobbotsch@VSadov