Structured concurrency for the JVM without the magic
A Java-first coroutine library bringing structured concurrency, cooperative cancellation, and message-passing channels to any JVM project. No compiler plugins, no runtime bytecode manipulation - just clean APIs built on Virtual Threads and explicit context passing.
Current release: tech.robd:jcoroutines:0.1.0
Documentation: https://robd.tech/jcoroutines
- Structured Concurrency: Parent-child relationships ensure no leaked tasks
- Cooperative Cancellation: Explicit cancellation checks, not thread interruption
- Message Passing: Type-safe channels with backpressure handling
- No Magic: Everything is explicit - contexts are passed as parameters
- Java 21+ (uses Virtual Threads)
- No additional runtime dependencies
dependencies {
implementation 'tech.robd.jcoroutines:jcoroutines-core:0.1.0'
}
<dependency>
<groupId>tech.robd</groupId>
<artifactId>jcoroutines</artifactId>
<version>0.1.0</version>
</dependency>
import tech.robd.jcoroutines.*;
// Simple async operation
JCoroutineHandle<String> handle = Coroutines.async(suspend -> {
suspend.delay(100);
return "Hello, Coroutines!";
});
String result = handle.join();
System.out.println(result);
try (var scope = new StandardCoroutineScope()) {
// Launch multiple operations
var handle1 = scope.async(suspend -> fetchData(suspend, "url1"));
var handle2 = scope.async(suspend -> fetchData(suspend, "url2"));
// Wait for results
String result1 = handle1.join();
String result2 = handle2.join();
} // Automatic cleanup and cancellation
try (var scope = new StandardCoroutineScope()) {
var channel = InteropChannel.<String>buffered(10);
// Producer
scope.launch(suspend -> {
for (int i = 0; i < 5; i++) {
channel.send(suspend, "Message " + i);
suspend.yieldAndPause(Duration.ofMillis(100));
}
channel.close();
});
// Consumer
scope.launch(suspend -> {
channel.forEach(suspend, (ctx, msg) -> {
System.out.println("Received: " + msg.orElse("null"));
});
});
}
A "suspend function" takes SuspendContext
as its first parameter:
public String fetchData(SuspendContext suspend, String url) {
suspend.checkCancellation(); // Check if cancelled
suspend.yieldAndPause(Duration.ofMillis(10)); // Be cooperative
return httpClient.get(url);
}
JCoroutines requires explicit cooperation:
suspend.yield()
- Basic yielding to other coroutinessuspend.yieldAndPause(Duration.ofMillis(10))
- Recommended for cooperative taskssuspend.delay(Duration.ofSeconds(1))
- Actual time delays
All operations check cancellation explicitly:
public void longTask(SuspendContext suspend) {
for (int i = 0; i < 1000; i++) {
suspend.checkCancellation(); // Throws CancellationException if cancelled
doWork(i);
if (i % 100 == 0) {
suspend.yieldAndPause(Duration.ofMillis(5)); // Be cooperative
}
}
}
@Service
public class DataService {
public CompletableFuture<List<String>> processAsync(List<String> items) {
return Coroutines.asyncAndForget(suspend -> {
try (var scope = new StandardCoroutineScope()) {
var handles = items.stream()
.map(item -> scope.async(ctx -> process(ctx, item)))
.toList();
return handles.stream()
.map(JCoroutineHandle::join)
.toList();
}
});
}
}
// Clean shutdown of global scopes
Runtime.getRuntime().addShutdownHook(new Thread(Coroutines::shutdown));
- Read the Examples for getting started examples
Apache 2.0