/await4j

Simplify asynchronous programming in Java with Project Loom virtual threads and a familiar async/await style API

Primary LanguageJavaMIT LicenseMIT

Await4J

Maven Central GitHub License Java CI with Maven CodeQL

awaiting.webp Simplify Java async programming with virtual threads using an async/await style API.

TL;DR: Features

The Async class provides utility methods for executing code asynchronously on virtual threads. It simplifies handling of asynchronous operations and exceptions and provides familiar API.

  • await(() -> {/* blocking code */}): Executes a code block on a virtual thread. Handles checked exceptions and wraps them into RuntimeException. RuntimeException and Errorare thrown as it is. If any other unexpected Throwable occurs, it throws an IllegalStateException.

  • await(Callable<T> block): Executes a Callable<T> block on a virtual thread and returns the result. Handles InterruptedException, ExecutionException, and other general exceptions by wrapping them in a RuntimeException.

  • await(Future<T> future): Waits for the Future<T> to complete and returns its result. Internally calls await(Callable<T> block).

  • await(CompletableFuture<T> completableFuture): Waits for the CompletableFuture<T> to complete and returns its result. Internally calls await(Callable<T> block).

See Sample.java.

Background

Project Loom has introduced Virtual Threads, but the API requires some boilerplate code to use it effectively in real-life projects:

To run blocking code in a Virtual Thread, it should be wrapped in:

Thread.ofVirtual().start(() -> {
    // run some blocking code here
}).join()

When you need to get the execution result back, a common approach is to run it in an executor:

int returnFromVirtualThread() {
    try (final var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        final var task = executor.submit(() -> {
            // Do some expensive calculation here
            return 42; // Return result
        });
        return task.get(); // Get result from task
    } catch (ExecutionException e) {
        throw new RuntimeException(e);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException(e);
    }
}

The Better Async API

What if it were possible to use syntax similar to Javascript's async/await style?

This library introduces helpful utilities to simplify calls in Virtual Threads:

For example, to call a lambda function, even throwing exceptions, use:

import me.kpavlov.await4j.Async.await;
...

final var completed = new AtomicBoolean();

final var slowCalculation = () -> {
  // Do something slow in a virtual thread
  try {
    Thread.sleep(1000);
  } catch (InterruptedException e) {
    throw new RuntimeException(e);
  }
  // Set flag to "true" to indicate, that the task is completed
  completed.set(true);
};

await(slowCalculation);

// Verify that calculation has been completed
System.out.println("Completed: " + completed.get()); // "Completed: true"

To call a lambda that returns a value (Callable), use:

import me.kpavlov.await4j.Async.await;
...

final var result = await(() -> {
    // Do some expensive calculation here
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    // Return the result
    return 42; 
});
System.out.println("Result: " + result); // "Result: 42"

Lambdas may throw exceptions, unlike the java.lang.Runnable interface. All non-runtime exceptions are wrapped in java.lang.RuntimeException, and java.lang.Error will be re-thrown.

Wrapping CompletableFuture and Future

Using java.util.concurrent.CompletableFuture and java.util.concurrentFuture from the Java API is also simplified:

final CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
  // Do some expensive calculation here
  try {
    Thread.sleep(1000);
  } catch (InterruptedException e) {
    throw new RuntimeException(e);
  }
  // Return the result
  return 42;
});

final var completableFutureResult = await(completableFuture);
System.out.println("CompletableFuture result: " + completableFutureResult); // "Result: 42"

final var futureResult = await((Future<Integer>) completableFuture);
System.out.println("Future Result: " + futureResult); // "Result: 42"

See full list of requirements here.

Useful Utility Classes

  • Result<T> - A discriminated union that encapsulates a successful outcome with a value of type T or a failure with an arbitrary Throwable exception. Similar to Result in Kotlin.
  • ThrowingRunnable - Runnable, which can throw Exception

Final Notes

If you want to write better code on JVM, use Kotlin Coroutines. This library remains a simpler choice for Java projects where adopting or migrating to Kotlin is not feasible.

The library focuses on running blocking code on Virtual Threads without providing additional parallelism optimizations. If your IO operations are slow, they will not run faster. If a lambda takes one second to run, await(...) will also take approximately one second, but on a virtual thread.

How to Get Started

  1. Add project dependency. Latest version can be found on maven central repository:

    Maven:

    <!-- https://mvnrepository.com/artifact/me.kpavlov.await4j/await4j -->
    <dependency>
        <groupId>me.kpavlov.await4j</groupId>
        <artifactId>await4j</artifactId>
        <version>[LATEST]</version>
    </dependency>

    Gradle:

    // https://mvnrepository.com/artifact/me.kpavlov.await4j/await4j
      implementation("me.kpavlov.await4j:await4j:${await4jVersion}")
  2. Import methods from Async

    import me.kpavlov.await4j.Async.await;

Links

  • JEP 444: Virtual Threads -- Virtual threads are lightweight threads that dramatically reduce the effort of writing, maintaining, and observing high-throughput concurrent applications.
  • JEP 480: Structured Concurrency -- Simplify concurrent programming by introducing an API for structured concurrency. Structured concurrency treats groups of related tasks running in different threads as a single unit of work, thereby streamlining error handling and cancellation, improving reliability, and enhancing observability. This is a preview API.
  • JEP 429: ScopedValues -- Scoped values enables sharing of immutable data within and across threads. They are preferred to thread-local variables, especially when using large numbers of virtual threads. This is an incubating API.
  • Java Async-Await -- Async-Await support for Java CompletionStage.