Use virtual threads to write Vert.x code that looks like it is synchronous.
You still write the traditional Vert.x code processing events, but you have the opportunity to write synchronous code for complex workflows and use thread locals in such workflows.
One of the key advantages of Vert.x over many legacy application platforms is that it is almost entirely non-blocking (of kernel threads) - this allows it to handle a lot of concurrency (e.g. handle many connections, or messages) using a very small number of kernel threads, which allows it to scale very well.
The non-blocking nature of Vert.x leads to asynchronous APIs. Asynchronous APIs can take various forms including callback style, promises or Rx-style. Vert.x uses futurese in most places (although, it also supports Rx).
In some cases, programming using asynchronous APIs can be more challenging than using a direct synchronous style, in particular if you have several operations that you want to do in sequence. Also, error propagation is often more complex when using asynchronous APIs.
Vertx virtual threads allows you to work with asynchronous APIs, but using a direct synchronous style that you’re already familiar with.
It does this by using Java 21 virtual threads. Virtual threads are very lightweight threads that do not correspond to underlying kernel threads. When they are blocked they do not block a kernel thread.
To use the virtual threads with Vert.x add the following dependency to the dependencies section of your build descriptor:
-
Maven (in your
pom.xml
):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-virtual-threads</artifactId>
<version>4.5.0-SNAPSHOT</version>
</dependency>
-
Gradle (in your
build.gradle
file):
dependencies {
compile 'io.vertx:vertx-virtual-threads:4.5.0-SNAPSHOT'
}
You can deploy virtual thread verticles.
A virtual thread verticle is a worker verticle that requires only a single instance of the verticle to run the application.
When the verticle awaits a result, the verticle can still process events like an event-loop verticle.
AbstractVerticle verticle = new AbstractVerticle() {
@Override
public void start() {
HttpClient client = vertx.createHttpClient();
HttpClientRequest req = Async.await(client.request(
HttpMethod.GET,
8080,
"localhost",
"/"));
HttpClientResponse resp = Async.await(req.send());
int status = resp.statusCode();
Buffer body = Async.await(resp.body());
}
};
// Run the verticle a on virtual thread
vertx.deployVerticle(verticle, new DeploymentOptions()
.setWorker(true)
.setInstances(1)
.setWorkerOptions(new VirtualThreadOptions()));
You can use Async.await
to suspend the current virtual thread until the awaited result is available.
The virtual thread is effectively blocked, but the application can still process events.
When a virtual thread awaits for a result and the verticle needs to process a task, a new virtual thread is created to handle this task.
When the result is available, the virtual thread execution is resumed and scheduled after the current task is suspended or finished.
Like any verticle at most one task is executed at the same time.
You can await on a Vert.x Future
Buffer body = Async.await(response.body());
or on a JDK CompletionStage
Buffer body = Async.await(response.body().toCompletionStage());
You can acquire the ownership of a java.util.concurrent.locks
Async.lock(theLock);
try {
//
} finally {
theLock.unlock();
}
A virtual thread verticle can interact safely with fields before an await
call. However, if you are reading a field before an await
call and reusing the value after the call, you should keep in mind that this value might have changed.
int value = counter;
value += Async.await(getRemoteValue());
// the counter value might have changed
counter = value;
You should read/write fields before calling await
to avoid this.
counter += Async.await(getRemoteValue());
Note
|
this is the same behavior with an event-loop verticle |
When your application blocks without using await
, e.g. using ReentrantLock#lock
, the Vert.x scheduler is not aware of it and cannot schedule events on the verticle: it behaves like a regular worker verticle, yet using virtual threads.
This use case is not encouraged but it is not forbidden, however the verticle should be deployed with several instances to deliver the aimed concurrency, like a regular worker verticle.
Thread locals are only reliable within the execution of a context task.
ThreadLocal<String> local = new ThreadLocal();
local.set(userId);
HttpClientRequest req = Async.await(client.request(HttpMethod.GET, 8080, "localhost", "/"));
HttpClientResponse resp = Async.await(req.send());