Skip to content

esoco/coroutines

Repository files navigation

Java Coroutines

Build Status

This project contains a pure Java implementation of coroutines. I has a single dependency to the ObjectRelations project.

It can be build locally after cloning by starting a gradle build with gradlew build.

Usage

To include coroutines into a project, add the dependency to your project.

Gradle

dependencies {
    implementation "de.esoco:coroutines:${esocoCoroutinesVersion}"
}

Maven

<dependencies>
    <dependency>
        <groupId>de.esoco</groupId>
        <artifactId>coroutines</artifactId>
        <version>${esoco.coroutines.version}</version>
    </dependency>
</dependencies>

The following gives only a short overview of how to use this project. More detailed information can be found on our documentation site and in the generated Javadoc.

Declaring Coroutines

Coroutines are used in two stages: the first is the declaration of coroutines, the second their execution. To declare coroutines this framework provides a simple builder pattern that starts with the invocation of the static factory method Coroutine.first(CoroutineStep). This creates a new coroutine instance that invokes a certain execution step. Afterward the coroutine can be extended with additional steps by invoking the instance method Coroutine.then(CoroutineStep). Each call of then() will return a new coroutine instance because coroutines are immutable. That allows to use existing coroutines as templates for derived coroutines.

Coroutine Steps

The execution steps of a coroutine are instances of the abstract class CoroutineStep. The sub-package step contains several pre-defined step implementations as well as several asynchronous I/O steps in the sub-packagestep.nio. All framework steps have static factory methods that can be used in conjunction with the coroutine builder methods and by applying static imports to declare coroutines with a fluent API. A simple example based on the step CodeExecution which executes functional expressions would look like this:

import static de.esoco.coroutine.Coroutine.*;
import static de.esoco.coroutine.step.CodeExecution.*;

Coroutine<String, Integer> parseInteger =
    Coroutine.first(apply((String s) -> s.trim()))
             .then(apply(s -> Integer.valueOf(s)));

Executing Coroutines

The execution of coroutines follows the pattern of structured concurrency and therefore requires an enclosing CoroutineScope. Like coroutines the scope provides a static factory method that receives a functional interface implementation which contains the code to run. That code can then run arbitrary coroutines asynchronously in the scope like in this example:

Coroutine<?, ?> crunchNumbers =
    first(run(() -> Range.from(1).to(10).forEach(Math::sqrt)));

CoroutineScope.launch(scope -> {
    // start a million coroutines
    for (int i = 0; i < 1_000_000; i++) {
        crunchNumbers.runAsync(scope);
    }
}); 

Any code that would follow after the scope launch block will only run after all coroutines have finished execution, either regularly or with an error. In the latter case the scope would also throw an exception if at least one coroutine fails.

Continuation

Each start of a coroutine produces a Continuation object that can be used to observe and if necessary cancel the asynchronous coroutine execution. When the coroutine has finished execution it provides access to the produced result (if such exists).

ScopeFuture

If a coroutine scope needs to produce a value it can be started by means of the produce method instead of launch. That method returns immediately with an instance of java.util.concurrent.Future which can then be used to monitor the scope execution and to query the result or error state after completion. This method can also be used if handling a scope through a future is preferred over a launch block. In any case the execution of the asynchronous code is contained and can be supervised by the application. A simple example:

Future<String> futureResult =
    CoroutineScope.produce(scope -> getScopeResult(scope), scope -> someCoroutine.runAsync(scope, "input"));

String result = futureResult.get();

Project Structure

The main package of this project is de.esoco.coroutine. It contains the core classes of the framework:

  • Coroutine: Serves to declare new coroutines which consist of sequential steps to be executed. Contains the static factory method first(CoroutinesStep) to create new declarations and the instance method then(CoroutineStep) to extend an existing coroutine. Coroutine instances are always immutable and extending them will always create a new instance.
  • CoroutineStep: The base class for all execution steps in coroutines.
  • CoroutineScope: Following the pattern of structured concurrency, all coroutines must be run from within a coroutine scope. The scope provides a supervision context that enforces the tracking of coroutine and scope termination, either successfully or by cancellation or error.
  • CoroutineContext: Coroutines and scopes are executed in a certain context that provides configuration and defines the threading environment.
  • Continuation: The execution of a coroutine is always associated with a dedicated continuation instance that allows the coroutine steps to share state. It also provides access to the scope and context of the respective execution.
  • Suspension: Coroutines can be suspended while waiting for resources. This class contains the state associated with the suspension and allows to resume the execution when the suspension condition is resolved.
  • Selection: A Suspension subclass that suspends on multiple executions and is used for the implementation of selecting steps.
  • Channel: This class implements a queue that allow multiple coroutines to perform suspending communication by sending to and receiving from channels.
  • ChannelId: An interface that is used for the identification of channels.

Furthermore the main package contains several exception classes that are used by the framework. All exceptions are unchecked and should therefore always be considered in an application's exception handling.

The sub-package step contains several standard coroutine step implementations. All step classes provides factory methods that can be used for a fluent declaration of coroutines based on the methods

  • CodeExecution: Executes code that is provided as a function as a single coroutine step. It is mainly intended to wrap lambda expressions or method references and provides factory methods based on the different functional interface types of Java: apply(Function), consume(Consumer), supply(Supplier), run(Runnable).
  • CallSubroutine: Runs another coroutine in the continuation of the current one.
  • Condition: Checks a logical condition by applying a predicate and executes different step according to the result.
  • Loop: Repeats the execution of a certain step (including subroutines) until a logical condition is met.
  • Iteration: Repeats the execution of a certain step (including subroutines) for all element in an Iterable input value.
  • Delay: Suspends the execution of a coroutine for a certain amount of time.
  • Select and Collect: Suspend the execution of a coroutine until one or all of a set of other coroutines have finished.
  • ChannelSend and ChannelReceive: These steps suspend the execution of a coroutine until sending to or receiving from a channel succeeded.

Finally, the sub-package step.nio contains several step implementations that use the asynchronous APIs of the java.nio package to implement suspending I/O functionality.

The folder src/examples contains some examples for using coroutines.

License

This project is licensed under the Apache 2.0 license (see LICENSE file for details).