|
1 | 1 | # Netty VirtualThread Scheduler |
2 | 2 |
|
3 | 3 | ## Introduction |
4 | | -This project provides an advanced integration between Java Virtual Threads (Project Loom) and Netty's event loop, enabling seamless execution of blocking operations within the Netty event loop itself. Unlike standard approaches, which require offloading blocking tasks to external thread pools (incurring multiple thread hand-offs), this scheduler allows blocking code to run directly on the event loop's carrier thread, leveraging the virtual thread execution model. |
| 4 | +This project provides an integration between Java Virtual Threads (Project Loom) and Netty's event loop, enabling low-overhead execution of blocking or CPU-bound work started from the Netty event loop without the usual extra thread hand-offs. |
5 | 5 |
|
6 | | -## Motivation |
7 | | -In traditional Netty applications, blocking operations (e.g., JDBC, file I/O) must not run on the event loop. The usual workaround is: |
8 | | -1. Offload the blocking task to an external thread pool (often using the default virtual thread scheduler). |
9 | | -2. Once complete, hand control back to the Netty event loop to continue processing (e.g., send a response). |
| 6 | +At a high level: |
| 7 | +- Each Netty event loop is backed by a virtual thread (the "event loop virtual thread"). |
| 8 | +- Each such virtual thread is executed on a dedicated carrier platform thread. |
| 9 | +- Virtual threads created from the event-loop-specific ThreadFactory returned by the group will be associated with the same EventLoopScheduler and, when possible, will run on the same carrier platform thread as the event loop. |
10 | 10 |
|
11 | | -This process involves at least two thread hand-offs, which not only increases latency and complexity, but also wastes CPU cycles due to waking up both the external thread pool and the event loop again. Additionally, moving data between threads harms cache locality, reducing cache friendliness and overall performance. |
| 11 | +This allows code that must block (for example, blocking I/O or synchronous library calls) to be executed without moving work between unrelated threads, reducing wake-ups and improving cache locality compared to offloading to an external thread pool. |
12 | 12 |
|
13 | | -## Technical Approach |
14 | | -- The Netty event loop runs as a special, long-lived virtual thread. |
15 | | -- Blocking operations issued from the event loop are executed as continuations, scheduled to run on the same platform thread (the "carrier")—unless work-stealing occurs (not implemented at the moment). |
16 | | -- Any virtual thread created from the event loop will, by default, run on the same carrier platform thread (again, unless work-stealing is introduced). |
17 | | -- This enables blocking libraries to be used transparently, without extra thread pools or hand-offs. |
| 13 | +## Key behavior (user-facing) |
| 14 | +- Create a `VirtualMultithreadIoEventLoopGroup` like any other Netty `EventLoopGroup`. It behaves like a `MultiThreadIoEventLoopGroup` from the Netty API, but the event loops are driven by virtual threads and coordinated with carrier platform threads by a per-event-loop `EventLoopScheduler`. |
| 15 | +- Call `group.vThreadFactory()` to obtain a `ThreadFactory` that creates virtual threads tied to an `EventLoopScheduler` of the group. When those virtual threads block, the scheduler parks them and resumes them later using the carrier thread. |
| 16 | +- Virtual threads created via the group's `vThreadFactory()` will attempt to inherit the scheduler and run with low-overhead handoffs back to the event loop when continuing work. |
| 17 | +- Virtual threads created with `Thread.ofVirtual().factory()` (the JVM default factory) do NOT automatically inherit the group's scheduler. |
| 18 | +- The library requires a custom global virtual thread scheduler implementation to be installed: set `-Djdk.virtualThreadScheduler.implClass=io.netty.loom.NettyScheduler` (or use the convenience helpers in the code/tests that set up the scheduler). |
| 19 | +- Blocking I/O support that relies on per-carrier pollers currently depends on the JVM poller mode (the code checks `jdk.pollerMode==3`). See notes below. |
18 | 20 |
|
19 | | -## Comparison Table |
| 21 | +## When to use this |
| 22 | +- You have code that must perform blocking operations from a Netty handler and you want to avoid an extra thread hand-off. |
| 23 | +- You want fewer CPU wake-ups and better cache locality by keeping related work on the same carrier platform thread. |
20 | 24 |
|
21 | | -| Aspect | Standard Netty + Loom (Default) | Netty with VirtualThread Scheduler (This Project) | |
22 | | -|-----------------------|-----------------------------------------|--------------------------------------------------| |
23 | | -| Blocking Operation | Offloaded to external thread pool | Runs as continuation on event loop carrier | |
24 | | -| Thread Hand-offs | 2+ | 0 | |
25 | | -| Cache Friendliness | Poor (data moves between threads) | High (data stays on carrier thread) | |
26 | | -| CPU Wakeups | More (wakes both pools) | Fewer (single carrier thread) | |
| 25 | +Caveats: |
| 26 | +- This project leverages experimental JVM features (Project Loom) and assumes recent Java versions (Java 21+ recommended). |
| 27 | +- You must install the Netty-specific scheduler (see above) for `VirtualMultithreadIoEventLoopGroup` to be usable. |
27 | 28 |
|
28 | | -## Architecture Diagrams |
| 29 | +## Usage example (simple) |
29 | 30 |
|
| 31 | +See the runnable example and step-by-step instructions in the example module: |
| 32 | + |
| 33 | +- example-echo/README.md |
| 34 | + |
| 35 | +(That README contains the minimal build and run commands, including how to start the example server and run a quick curl smoke test.) |
| 36 | + |
| 37 | +## About the Loom build used |
| 38 | + |
| 39 | +This work targets Project Loom features and was developed and tested against very recent OpenJDK / Loom builds. If you don't want to build OpenJDK yourself, a convenient set of prebuilt Loom-enabled JDK images is available from Shipilev's builds: |
| 40 | + |
| 41 | +- https://builds.shipilev.net/openjdk-jdk-loom/ |
| 42 | + |
| 43 | +Follow that site to download a suitable JDK image for your platform and point `JAVA_HOME` to the JDK image before running the project. Example: |
| 44 | + |
| 45 | +```sh |
| 46 | +export JAVA_HOME=/path/to/loom/build/linux-x86_64-server-release/jdk/ |
30 | 47 | ``` |
31 | | -Standard Netty + Loom (Default): |
32 | | -
|
33 | | -┌──────────────┐ offload ┌────────────────────────────┐ callback ┌──────────────┐ |
34 | | -│ EventLoop │───────────▶│ Virtual Thread (Scheduler) │────────────▶│ EventLoop │ |
35 | | -│ (OS Thread) │ │ (External Thread Pool) │ │ (OS Thread) │ |
36 | | -└──────────────┘ └────────────────────────────┘ └──────────────┘ |
37 | | -
|
38 | | -Netty with VirtualThread Scheduler: |
39 | | -
|
40 | | -┌────────────────────────────────────────────┐ |
41 | | -│ Platform Thread (Carrier) │ |
42 | | -│ ┌──────────────────────────────────────┐ │ |
43 | | -│ │ EventLoop (Virtual Thread) │ │ |
44 | | -│ │ (runs on carrier platform thread) │ │ |
45 | | -│ │ ──────────────────────────────── │ │ |
46 | | -│ │ Blocking Operation │ │ |
47 | | -│ │ (Continuation, same platform) │ │ |
48 | | -│ └──────────────────────────────────────┘ │ |
49 | | -└────────────────────────────────────────────┘ |
| 48 | + |
| 49 | +If you prefer to build from the OpenJDK `loom` repository yourself, the upstream source is: |
| 50 | + |
| 51 | +- https://github.com/openjdk/loom |
| 52 | + |
| 53 | +If you have a local build of the latest Loom-enabled OpenJDK, point `JAVA_HOME` to that build before running tests and benchmarks. Example: |
| 54 | + |
| 55 | +```sh |
| 56 | +export JAVA_HOME=/path/to/your/loom/build/linux-x86_64-server-release/jdk/ |
| 57 | +mvn clean install |
50 | 58 | ``` |
51 | 59 |
|
52 | | -## Build and Run |
| 60 | +## Integration tips and runtime flags |
| 61 | +- Install the Netty scheduler as the JVM virtual-thread scheduler with: |
| 62 | + -Djdk.virtualThreadScheduler.implClass=io.netty.loom.NettyScheduler |
| 63 | + |
| 64 | +- Some blocking I/O integrations rely on per-carrier pollers. The code checks `jdk.pollerMode` and expects value `3` for per-carrier pollers. This can be controlled via JVM flags or defaults depending on your JVM version. |
| 65 | + |
| 66 | +- Use `group.vThreadFactory()` from inside Netty handlers if you want the spawned virtual thread to be associated with the same event loop scheduler as the handler's event loop. |
53 | 67 |
|
| 68 | +- If you need the virtual thread to inherit the scheduler when forking tasks via StructuredTaskScope, pass the group's `vThreadFactory()` to the scope's `withThreadFactory(...)`. |
| 69 | + |
| 70 | +## Build and Run |
54 | 71 | This project uses Maven for build and dependency management. |
55 | 72 |
|
56 | | -1. **Build the project:** |
57 | | - ```sh |
58 | | - mvn clean install |
59 | | - ``` |
60 | | -2. **Run Benchmarks:** |
61 | | - ```sh |
62 | | - cd benchmarks |
63 | | - mvn clean install |
64 | | - java -jar target/benchmarks.jar |
65 | | - ``` |
| 73 | +1. Build the project: |
| 74 | + |
| 75 | +```sh |
| 76 | +mvn clean install |
| 77 | +``` |
| 78 | + |
| 79 | +2. Run the benchmarks (optional): |
| 80 | + |
| 81 | +```sh |
| 82 | +cd benchmarks |
| 83 | +mvn clean install |
| 84 | +java -jar target/benchmarks.jar |
| 85 | +``` |
66 | 86 |
|
67 | 87 | ## Prerequisites |
68 | 88 | - Java 21 or newer (with Loom support) |
69 | 89 | - Maven 3.6+ |
| 90 | +- To use the `VirtualMultithreadIoEventLoopGroup` set the JVM property to install the Netty scheduler: |
| 91 | + -Djdk.virtualThreadScheduler.implClass=io.netty.loom.NettyScheduler |
70 | 92 |
|
71 | 93 | ## References |
72 | | -- [Project Loom (OpenJDK)](https://openjdk.org/projects/loom/) |
73 | | -- [Netty Project](https://netty.io/) |
| 94 | +- Project Loom (OpenJDK): https://openjdk.org/projects/loom/ |
| 95 | +- Netty Project: https://netty.io/ |
74 | 96 |
|
75 | 97 | --- |
76 | 98 | For more details, see the source code and benchmark results in the respective modules. |
0 commit comments