diff --git a/README.md b/README.md index de666f3..7aaef58 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,26 @@
Tame your async — no platform threads were harmed.
-A tiny Java 21 library that lets you wait for async results on virtual threads, -so your platform threads stay free and your context tags along for the ride. +A tiny Java 21 library for virtual thread concurrency. Two things: -## Why +1. **`Blockless.get()`** — wait for async results safely, without entering the future's internals +2. **`Parallel`** — fan out work on virtual threads, collect results, keep your trace context + +So your platform threads stay free and your context tags along for the ride. + +Zero dependencies in core. Pluggable context propagation for MDC, gRPC, and OpenTelemetry. + +## Quick start + +```java +// Wait for a CompletionStage without blocking platform threads +var result = Blockless.get(CompletableFuture.supplyAsync(() -> "hello")); + +// Run a Callable on a virtual thread and get the result +var answer = Blockless.get(() -> expensiveComputation()); +``` + +## Why `Blockless.get()` `CompletableFuture.join()` parks your thread inside the future's own internals. If that implementation uses `synchronized` or you're on a platform thread, you @@ -27,19 +43,42 @@ String result = someFuture.join(); String result = Blockless.get(someFuture); ``` -## Quick start +Also works with callables — runs them on a virtual thread and blocks for the result: ```java -// Wait for a CompletionStage without blocking platform threads -var result = Blockless.get(CompletableFuture.supplyAsync(() -> "hello")); - -// Run a Callable on a virtual thread and get the result var answer = Blockless.get(() -> expensiveComputation()); ``` -## Parallel execution +## Why `Parallel` -Run work concurrently on virtual threads with context propagation built in: +Services often need to fan out N calls and collect the results. The typical +pattern looks like this: + +```java +// Before: manual executor + supplyAsync + semaphore + join +var executor = Executors.newVirtualThreadPerTaskExecutor(); +var semaphore = new Semaphore(10); +var futures = ids.stream() + .map(id -> CompletableFuture.supplyAsync(() -> { + semaphore.acquireUninterruptibly(); + try { return fetchById(id); } + finally { semaphore.release(); } + }, executor)) + .toList(); +var results = futures.stream().map(CompletableFuture::join).toList(); +``` + +With blockless: + +```java +// After: one line, same behavior +var results = parallel.withMaxConcurrency(10).map(ids, this::fetchById); +``` + +Each task runs on its own virtual thread. Results stay in input order. +MDC and trace context survive the hop. + +### Usage ```java var parallel = Parallel.create(new Slf4jMdcContextPropagator()); @@ -47,27 +86,43 @@ var parallel = Parallel.create(new Slf4jMdcContextPropagator()); // Map in parallel, results stay in order List