Skip to content

feat: virtual thread executor support #32689

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 7, 2025
Merged

Conversation

johanandren
Copy link
Contributor

@johanandren johanandren commented Apr 1, 2025

This adds a new executor config which will make a dispatcher run each scheduled task as a separate, new, virtual thread. This is very useful for blocking logic, for example in futures, but less so for running actors on and has considerable overhead for message intense systems.

Somewhat related issue #31613

throw new IllegalStateException("Virtual thread executors only supported on JDK 21 and newer")
}
val ofVirtual = ofVirtualMethod.invoke(null)
// java.lang.ThreadBuilders.VirtualThreadBuilder is package private
Copy link
Contributor

Choose a reason for hiding this comment

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

How would you write this without reflection?

Copy link
Contributor

Choose a reason for hiding this comment

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

isn't the public way like this?

ThreadFactory virtualThreadFactory = Thread.ofVirtual().factory();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, that's what we are calling here, with a potential name inbetween Thread.ofVirtual().name(virtualThreadName).factory()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To clarify the comment, the reason why we can't just do ofVirtual.getClass.getMethod("name") is that it is of a package private type, so we need to look up the interface it implements that has the name method instead.

Copy link
Contributor

@patriknw patriknw Apr 4, 2025

Choose a reason for hiding this comment

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

I was thinking we could use Thread.Builder.OfVirtual, which is the public type. I think this should work:

val ofVirtual = ofVirtualMethod.invoke(null)
val nameMethod = classOf[Thread.Builder.OfVirtual].getMethod("name", classOf[String])
nameMethod.invoke(ofVirtual, virtualThreadName)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thread.Builder.OfVirtual only exists on JDK 21 and later so we can't do classOf[Thread.Builder.OfVirtual] and have the code still compile on 11 and 17

Copy link
Contributor

@patriknw patriknw Apr 4, 2025

Choose a reason for hiding this comment

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

sorry, above comment about ThreadBuilders.VirtualThreadBuilder was misleading me. I thought you reached out for that one. You are already using the one that I suggested.

val ofVirtualInterface = dynamicAccess.getClassFor[AnyRef]("java.lang.Thread$Builder$OfVirtual").get

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I'll drop the comment, it's not needed and worse if it is confusing :)

@johanandren johanandren marked this pull request as ready for review April 2, 2025 17:02
@johanandren
Copy link
Contributor Author

For the record, here are some results of ActorBenchmark comparing direct JFP with virtual threads to see the overhead (Linux, 12 core x86 intel i5):

akka-bench-jmh/jmh:run -f 1 -wi 10 -i 10 .*actor\.ActorBenchmark
[info] Benchmark            (batchSize)    (dispatcher)                                         (mailbox)  (tpt)   Mode  Cnt          Score         Error  Units
[info] ActorBenchmark.echo           50  fjp-dispatcher  akka.dispatch.SingleConsumerOnlyUnboundedMailbox     50  thrpt   10  202 427 509.824 ± 4177250.804  ops/s
[info] ActorBenchmark.echo           50   vt-dispatcher  akka.dispatch.SingleConsumerOnlyUnboundedMailbox     50  thrpt   10  145 382 846.167 ±  288754.289  ops/s

I've also run a benchmark looking at gc, sadly did not save the output, but alloc rate up from 0.001 B per op to 0.309 B, not scaled with troughput: 27mb/s for FJP up to 48mb/s with virtual thread per task, this leads to around 10% more time spent on GC.

This proves that it is maybe not a good default default dispatcher for actors, however for a blocking io dispatcher that will anyway have a much lower throughput, mostly waiting for some blocking task, it seems like a reasonable tradeoff.

throw new IllegalStateException("Virtual thread executors only supported on JDK 21 and newer")
}
val ofVirtual = ofVirtualMethod.invoke(null)
// java.lang.ThreadBuilders.VirtualThreadBuilder is package private
Copy link
Contributor

@patriknw patriknw Apr 4, 2025

Choose a reason for hiding this comment

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

I was thinking we could use Thread.Builder.OfVirtual, which is the public type. I think this should work:

val ofVirtual = ofVirtualMethod.invoke(null)
val nameMethod = classOf[Thread.Builder.OfVirtual].getMethod("name", classOf[String])
nameMethod.invoke(ofVirtual, virtualThreadName)

Copy link
Contributor

@patriknw patriknw left a comment

Choose a reason for hiding this comment

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

LGTM, with possibly changing misleading comment

throw new IllegalStateException("Virtual thread executors only supported on JDK 21 and newer")
}
val ofVirtual = ofVirtualMethod.invoke(null)
// java.lang.ThreadBuilders.VirtualThreadBuilder is package private
Copy link
Contributor

@patriknw patriknw Apr 4, 2025

Choose a reason for hiding this comment

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

sorry, above comment about ThreadBuilders.VirtualThreadBuilder was misleading me. I thought you reached out for that one. You are already using the one that I suggested.

val ofVirtualInterface = dynamicAccess.getClassFor[AnyRef]("java.lang.Thread$Builder$OfVirtual").get

@johanandren johanandren merged commit dfca939 into main Apr 7, 2025
5 checks passed
@johanandren johanandren deleted the wip-loom-dispatcher-support branch April 7, 2025 08:42
@johanandren johanandren added this to the 2.10.3 milestone Apr 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants