Skip to content

Add VirtualThreadScheduler.currentVirtualThreadTask() default method#224

Open
franz1981 wants to merge 1 commit into
openjdk:fibersfrom
franz1981:current-vt-task-api
Open

Add VirtualThreadScheduler.currentVirtualThreadTask() default method#224
franz1981 wants to merge 1 commit into
openjdk:fibersfrom
franz1981:current-vt-task-api

Conversation

@franz1981

@franz1981 franz1981 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

I have worked on this change in order to test the custom scheduler impl at https://github.com/franz1981/Netty-VirtualThread-Scheduler to verify how it will behave while fully replacing the built-in scheduler, since I've implemented go-like work stealing on it.

The relevant custom scheduler impl change which shows the (unique) benefits of this API are at franz1981/Netty-VirtualThread-Scheduler@3eae86b , which are:

  • no need for a non-inheritable ScopedValue to know the current VirtualThread's running scheduler (if any)

  • to distinguish "internal" vs "external" submissions from VirtualThreads created via Thread.ofVirtual.start

  • I confirm that I make this contribution in accordance with the OpenJDK Interim AI Policy.


Progress

  • Change must not contain extraneous whitespace

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/loom.git pull/224/head:pull/224
$ git checkout pull/224

Update a local copy of the PR:
$ git checkout pull/224
$ git pull https://git.openjdk.org/loom.git pull/224/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 224

View PR using the GUI difftool:
$ git pr show -t 224

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/loom/pull/224.diff

Using Webrev

Link to Webrev Comment

Returns the VirtualThreadTask for the current virtual thread, allowing
custom scheduler implementations to read the task's attachment from
within a running VT. Returns null for platform threads.

Guarded with the same pattern as newThread(): throws
UnsupportedOperationException when called on the built-in scheduler.
@bridgekeeper

bridgekeeper Bot commented Jun 11, 2026

Copy link
Copy Markdown

👋 Welcome back franz1981! A progress list of the required criteria for merging this PR into fibers will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk

openjdk Bot commented Jun 11, 2026

Copy link
Copy Markdown

@franz1981 This change now passes all automated pre-integration checks.

ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details.

After integration, the commit message for the final commit will be:

Add VirtualThreadScheduler.currentVirtualThreadTask() default method

You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed.

At the time when this comment was updated there had been no new commits pushed to the fibers branch. If another commit should be pushed before you perform the /integrate command, your PR will be automatically rebased. If you prefer to avoid any potential automatic rebasing, please check the documentation for the /integrate command for further details.

As you do not have Committer status in this project an existing Committer must agree to sponsor your change.

➡️ To flag this PR as ready for integration with the above commit message, type /integrate in a new comment. (Afterwards, your sponsor types /sponsor in a new comment to perform the integration).

@franz1981 franz1981 marked this pull request as ready for review June 12, 2026 09:49
@openjdk openjdk Bot added ready Ready to be integrated rfr Ready for review labels Jun 12, 2026
@mlbridge

mlbridge Bot commented Jun 12, 2026

Copy link
Copy Markdown

Webrevs

@AlanBateman

AlanBateman commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Can you provide a summary on what you are looking for? Is this to avoid the scheduler/framework from maintaining its own map? If the scheduler has its own map then it would avoid the issues you listed.

There may be merit in the prototype exposing a virtual Thread to VirtualThreadTask lookup but I think we need good reason to do that first.

@franz1981

Copy link
Copy Markdown
Contributor Author

The attachment already keeps the scheduler context where it belongs: per-task, no shared state.
The issue is that a running VT currently has no way to read its own attachment.

The scheduler needs this in onStart: when a VT starts a child, the scheduler must know which carrier the caller is running on, to decide between local submission (like lazySubmit) and external submission.
Today I use a ScopedValue for this, but ScopedValues aren't inherited by Thread.ofVirtual().start() children and add overhead, contention, and state duplication.

With currentVirtualThreadTask(), the attachment becomes the single source of truth.
It allowed me to remove the ScopedValue entirely, preserving locality, avoiding contention and GC-related costs.
A ConcurrentHashMap would work functionally, but it reintroduces shared mutable state on every VT start and termination (the entry must be removed when the VT terminates, still hitting the shared map under contention with many VTs in flight across many cores).
That's exactly what the per-task attachment was designed to avoid.

Additionally, with this API the scheduler interface could potentially be simplified to a single execute(VirtualThreadTask) rather than separate onStart/onContinue.
The distinction between first start and continuation is fully encoded in the attachment's presence/user-defined states e.g. null means first submission, non-null means the context is already established.
Even the built-in FJP scheduler implements both methods identically.

@AlanBateman

Copy link
Copy Markdown
Collaborator

I don't think currentVirtualThreadTask is the right API, can you experiment with VirtualThreadTask virtualThreadTask(Thread thread) instead as that provide the mapping for the scheduler. It could be called with Thread.current() if needed.

Additionally, with this API the scheduler interface could potentially be simplified to a single execute(VirtualThreadTask) rather than separate onStart/onContinue.

As you might remember, we had to split this in order to deal with some cases (that was last year).

@franz1981

franz1981 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

VirtualThreadTask virtualThreadTask(Thread thread)

I think your proposal is just better! ❤️

As you might remember, we had to split this in order to deal with some cases (that was last year).

You're right; I remember there was a precise reason. I'll search my issues and report back here once I find it.

Looking at the code, I believe the split was related to recognizing per-carrier read pollers (pollerMode=3) at creation time, to assign them to the carrier of the VT that started them (while blocking on an I/O read op).

With virtualThreadTask(Thread), I think the split may no longer be necessary:

  • If the custom scheduler fully replaces the built-in (handles Thread.ofVirtual().start() too): read pollers are just VTs — the scheduler assigns them to a carrier the same way as any other VT without a custom factory. Retrieving the current thread's task via virtualThreadTask(Thread.currentThread()) is enough to make that decision.

  • If the custom scheduler doesn't handle all VTs: it needs to detect read pollers that aren't produced by its own factories. Today it checks the thread name at start time. But it could instead check whether the task has a null attachment — a VT with no attachment is not ours, and only then does it fall back to name-based detection.

In both cases, a single execute(VirtualThreadTask) entry point with attachment-based branching would suffice. The onStart/onContinue distinction becomes the scheduler's internal concern rather than an interface contract.
That said I will prototype it and make sure on the code about it 🙏

Do you want me to send a new PR for VirtualThreadTask virtualThreadTask(Thread thread)? Or I can modify the existing one? Probably the test will still work fine(ish) if you like them 🙏

@AlanBateman

Copy link
Copy Markdown
Collaborator

Do you want me to send a new PR for VirtualThreadTask virtualThreadTask(Thread thread)?

I think the starting point need s a clear explanation as why/how this would be used. You said "VT currently has no way to read its own attachment" but something isn't right if a virtual thread is interacting with the scheduler managed attachment outside the context of the scheduler code. The custom scheduler should already have the vthread <-> task mapping for all started virtual threads so it shouldn't need this, if you see what I mean.

@franz1981

franz1981 commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

Just to clarify — this is entirely within the scheduler code, not user code. The attachment is never accessed outside the scheduler.

The concrete case: inside onStart, I have the child's VirtualThreadTask, but I need the caller's scheduling context to decide local vs external submission (like FJP's lazySubmit vs externalSubmit). The caller is Thread.currentThread() — I have the Thread but no way to get its task.

I tried the CHM approach: I keep a Thread → VirtualThreadTask map only for "adopted" VTs (those created via Thread.ofVirtual().start(), not my factories). Insert on onStart, remove on termination. It works, but it's expensive.

I benchmarked it on handle-all-chm-hybrid branch — 128 short-lived VTs per op, 8 submitters, 16 cores, 2 NUMA nodes (-wi 5 -i 10 -f 3):

customPinWsPoller (factory, no CHM)   108,854 ± 6,040 ops/s
customReplaceBuiltin (CHM path)        85,436 ± 3,139 ops/s

CHM loses 21% to the factory path. perfasm shows where it goes:

  16.83%  ConcurrentHashMap::putVal      ← insert on every onStart
  13.70%  CacheStressBenchmark::cacheStressWithFactory  ← the actual work
   8.96%  ConcurrentHashMap::replaceNode ← remove on every termination

26% of cycles on CHM ops — nearly double the actual workload. perf c2c confirms it: 2.8x more Remote HITMs (cross-NUMA coherence) on the CHM path vs the factory path.

virtualThreadTask(Thread) would let me read the caller's attachment directly in onStart — no map, no lifecycle tracking, no shared mutable state. The runtime already knows which task belongs to which thread; this just exposes it through the same guarded pattern as newThread().

Benchmark code: CacheStressBenchmark.customReplaceBuiltin
Branch: handle-all-chm-hybrid

@AlanBateman

AlanBateman commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

The concrete case: inside onStart, I have the child's VirtualThreadTask, but I need the caller's scheduling context to decide local vs external submission (like FJP's lazySubmit vs externalSubmit).

Can you confirm that this only arises when using two virtual thread schedulers and not one? (by two I mean some some virtual threads are scheduled with your scheduler, others with the built-in). Asking because normally internal is the current thread is virtual thread, external otherwise.

@franz1981

Copy link
Copy Markdown
Contributor Author

I mean some some virtual threads are scheduled with your scheduler, others with the built-in

Yes, this is exactly that use case: I've introduced https://github.com/franz1981/Netty-VirtualThread-Scheduler#replace-built-in-scheduler-experimental in my custom scheduler as it now has a decent work-stealing implementation which makes it possible.

@viktorklang-ora

Copy link
Copy Markdown
Contributor

Another idea, if possible, is to augment onStart to have two parameters—the VirtualThreadTask of the parent, and the VirtualThreadTask of the child?

@franz1981

franz1981 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

That could work @AlanBateman but it won't still allow to access from within the "runnable" VT, to its own scheduling context:

  • this could be used for "explicit" routing decisions other than the default ones implemented by onStart i.e. the "scheduler" usually check if the current runnable queue is empty, but a custom thread factory could just enqueue it locally regardless it (e.g. Thread.OfVirtual.start would use the onStart one, while a custom scheduler factory could act differently, and expose something more "opinionated")
  • it is useful for testing: validate that a VT is running where it should, is key to many tests I built

I have another idea which could help on this too, and would cover the other gap related per carrier pollers detection.
I will send soon a draft pr for it 🙏 so you can tell me wdyt.

In any case (your proposal and the draft pr I will send you soon) still cause some duplication of info which doesn't feel right (the scoped value) as:

  • force allocating special scoped values which are biased toward the thread Id which own it (to save inheritance to make the context info to leak)
  • the info Is already available in the Thread, which makes odd to think an implementor should plan and manage any extra tracking

@AlanBateman

Copy link
Copy Markdown
Collaborator

Another idea, if possible, is to augment onStart to have two parameters—the VirtualThreadTask of the parent, and the VirtualThreadTask of the child?

That would be a better way to fit it into the current prototype API.

@franz1981

franz1981 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Sorry @AlanBateman and @viktorklang-ora - my comment at #224 (comment) was actually for @viktorklang-ora ^^

So far, related the scheduling gaps:

  1. the proposal of @viktorklang-ora helps with having an additional information (the caller's VTtask) which a custom scheduler could use to perform scheduling decisions in a "safe" place (onStart is pinned IIRC). I haven't explored other implementations so, "it works for me" and hopefully can be enough for others 🤞
  2. It's likely that onContinue would need the same parent/child info @viktorklang-ora : let's think what FJP does so far i.e. "JDK pollers calling "unpark" on other VT are capable to submit locally them". This is possible only because the scheduler is aware of the caller's "running context". But JDK pollers are not "managed" by a custom scheduler (they have no ScopedValue and their VTask is not accessible), so only providing the "parent" VTask would enable to do it.

Now, related, but not solved:

  • Unit tests or user-code could still need information to query the scheduling info of the current VT while it runs: if you feel that "while it runs" not to be safe, help me understand 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready Ready to be integrated rfr Ready for review

Development

Successfully merging this pull request may close these issues.

3 participants