Skip to content

Commit

Permalink
Improve documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
vietj committed Oct 24, 2023
1 parent 6c231ff commit 3931e10
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 100 deletions.
85 changes: 41 additions & 44 deletions src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ 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.

== Introduction

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.

== Usage

To use the virtual threads with Vert.x add the following dependency to the _dependencies_ section of your build descriptor:
Expand All @@ -27,92 +39,77 @@ dependencies {
}
----

== Getting started
== Using virtual threads

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.

[source,java]
----
{@link examples.VirtualThreadExamples#gettingStarted}
----

== What this is about
=== Blocking within a virtual thread verticle

Async/Await for Vert.x
You can use {@link io.vertx.virtualthreads.await.Async#await} to suspend the current virtual thread until the awaited result is available.

== What this is not about
The virtual thread is effectively blocked, but the application can still process events.

Blocking on other JDK blocking constructs such as latches, locks, sleep, etc...
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.

NOTE: it remains possible to block on these constructs using `Async.await`
When the result is available, the virtual thread execution is resumed and scheduled after the current task is suspended or finished.

== What you get
Like any verticle at most one task is executed at the same time.

By default, Vert.x dispatches events on an event-loop thread.
You can await on a Vert.x `Future`

[source,java]
----
{@link examples.VirtualThreadExamples#whatYouGet1}
----

Using virtual threads with Vert.x requires to run application tasks on a virtual threads

[source,java]
----
{@link examples.VirtualThreadExamples#whatYouGet2}
{@link examples.VirtualThreadExamples#awaitingFutures1}
----

This project implements virtual threads with Vert.x with a race free model. Events are dispatched to a virtual thread, when this virtual thread awaits an asynchronous result, the pending events are not dispatched until the virtual thread is resumed.
or on a JDK `CompletionStage`

[source,java]
----
{@link examples.VirtualThreadExamples#whatYouGet3}
{@link examples.VirtualThreadExamples#awaitingFutures2}
----

When a virtual thread awaits a future, a new virtual thread can be started to handle new events and avoid blocking the appliction or potential self deadlocks, e.g. in the following example, awaiting the response does not prevent the timer to fire
You can acquire the ownership of a `java.util.concurrent.locks`

[source,java]
----
{@link examples.VirtualThreadExamples#whatYouGet4}
{@link examples.VirtualThreadExamples#awaitingLocks1}
----

If you block a virtual thread without `{@link io.vertx.virtualthreads.await.Async`, your application then will behave like a regular worker and events will not be processed until the current task ends.
=== Field visibility

== Verticles

Virtual thread verticles are actually worker verticles, however a single worker instance is enough to execute it.
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.

[source,java]
----
{@link examples.VirtualThreadExamples#deployVerticle}
{@link examples.VirtualThreadExamples#fieldVisibility1}
----

== Supported primitives

=== Futures

You can await a Vert.x `Future`
You should read/write fields before calling `await` to avoid this.

[source,java]
----
{@link examples.VirtualThreadExamples#awaitingFutures1}
{@link examples.VirtualThreadExamples#fieldVisibility2}
----

or a JDK `CompletionStage`
NOTE: this is the same behavior with an event-loop verticle

[source,java]
----
{@link examples.VirtualThreadExamples#awaitingFutures2}
----
=== Blocking without await

=== Locks
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.

You can lock a `java.util.concurrent.locks`

[source,java]
----
{@link examples.VirtualThreadExamples#awaitingLocks1}
----
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 local support
=== Thread local support

Thread locals are only reliable within the execution of a context task.

Expand Down
87 changes: 31 additions & 56 deletions src/main/java/examples/VirtualThreadExamples.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,71 +23,47 @@
public class VirtualThreadExamples {

public void gettingStarted(Vertx vertx) {
Async async = new Async(vertx);
async.run(v -> {
// Run on a Vert.x a virtual thread
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());
});
}

public void whatYouGet1(HttpClientRequest request) {
request
.send()
.onSuccess(response -> {
// Set the buffer for handlers
response.handler(buffers -> {
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()));
}

public void whatYouGet2(HttpClientRequest request) {
Thread.startVirtualThread(() -> {
CompletableFuture<HttpClientResponse> fut = new CompletableFuture<>();
request.send().onComplete(ar -> {
if (ar.succeeded()) {
fut.complete(ar.result());
} else {
fut.completeExceptionally(ar.cause());
}
});;
try {
HttpClientResponse response = fut.get();
// As we get the response the virtual thread, there is a window of time where the event-loop thread has already sent buffers and we lost these events
response.handler(buffer -> {
private int counter;

});
} catch (Exception e) {
// Ooops
}
});
public void fieldVisibility1() {
int value = counter;
value += Async.await(getRemoteValue());
// the counter value might have changed
counter = value;
}

public void whatYouGet3(HttpClientRequest request) {
Future<HttpClientResponse> fut = request.send();
HttpClientResponse response = Async.await(fut);
// Buffer events might be in the queue and if they are, they will be dispatched next
response.handler(buffer -> {

});
public void fieldVisibility2() {
counter += Async.await(getRemoteValue());
}

public void whatYouGet4(Vertx vertx, HttpClientRequest request) {
Promise<HttpClientResponse> promise = Promise.promise();
vertx.setTimer(100, id -> promise.tryFail("Too late"));
request.send().onComplete(promise);
try {
HttpClientResponse response = Async.await(promise.future());
} catch (Exception timeout) {
// Too late
}
private Future<Buffer> callRemoteService() {
return null;
}

private Future<Buffer> callRemoteService() {
private Future<Integer> getRemoteValue() {
return null;
}

Expand Down Expand Up @@ -115,7 +91,6 @@ public void start() {
.setWorkerOptions(new VirtualThreadOptions()));
}


public void awaitingFutures1(HttpClientResponse response) {
Buffer body = Async.await(response.body());
}
Expand All @@ -133,7 +108,7 @@ public void awaitingLocks1(Lock theLock) {
}
}

public void threadLocalSupport1(Lock theLock, String userId, HttpClient client) {
public void threadLocalSupport1(String userId, HttpClient client) {
ThreadLocal<String> local = new ThreadLocal();
local.set(userId);
HttpClientRequest req = Async.await(client.request(HttpMethod.GET, 8080, "localhost", "/"));
Expand Down

0 comments on commit 3931e10

Please sign in to comment.