Exploring coroutines

This project explores the possibilities and problems of coroutines via ready to run examples.

Chapter 1 - Why use coroutines

There is a limit how many threads can be run (ThreadCount.kt), that is surprisingly low (depends on JVM memory).

Coroutines are more resource friendly (CoroutineCount.kt) and can be launched in much higher numbers (think many thousands).

Chapter 2 - Who executes coroutines

Coroutines still use threads, which can come from different sources (WhoExecutesMe.kt).

Blocking the thread used for coroutine execution defeats the purpose of using coroutines (FakeCoroutine.kt).

Chapter 3 - Handling blocking code

You cannot use code that blocks threads without special precautions, since any snippet that blocks the thread for a long time blocks a thread of the coroutine context. When there are no more threads left running, all coroutines will be blocked.

Solution 1: Explicitly launch a thread for each piece of blocking code (LaunchThread.kt). This can obviously only be done until we run into resource issues.

Solution 2: Run blocking code in a separate context. For example a fixed thread pool (SeparateContext.kt). This controls the amount of resources that can be used by blocking code but makes it difficult to synchronize with the non-blocking code.

Solution 3: Decouple the blocking code in something like a worker or actor (Decouple.kt). This is essentially the same as solution 2 with an additional channel for communication.

Chapter 4 - The launch() method

The launch() method needs a context, but is not suspending (LaunchDoesNotSuspend.kt).

In a single thread context coroutines created by launch() will only be started once the calling coroutine suspends, e.g. via yield() (ManuallySuspend.kt).

The launch() method returns a Job instance, which can be used to control or query the coroutine's state (LaunchReturnsAJob.kt).

A context with multiple threads might execute the launched coroutine at any time (LaunchMayBeStartedByAnotherThread.kt).

Using async() is just like launch() but it can return a value inside an instance of Deferred, which derives from Job so also allows control over the coroutine (AsyncIsLikeLaunchButReturnsAValue.kt).

Chapter 5 - The CoroutineScope

Running inside runBlocking() blocks the current thread and uses it to execute all coroutines inside it (see UsingRunBlocking.kt).

Calling coroutineScope() suspends the current coroutine until the execution inside it terminates, it acts like runBlocking() but inside an existing coroutine. It can also return a value (see UsingCoroutineScope.kt).

A scope can be created without blocking and outside an existing coroutine (see ManualScope.kt).

Chapter 6 - Cancellation

Throwing an exception in a coroutine cancels all still running coroutines in the same scope. This includes parents, siblings and children (see ExceptionCancellation.kt).

Cancelling a coroutine manually (or throwing a CancellationException) will only cancel children that are not completed (see ManualCancellation.kt).

Running a coroutine as a child of a SupervisorJob will stop any cancellation to propagate out of that coroutine (see SupervisorJobCancellation.kt).

Chapter 7 - Channels

By default, channels have a capacity of RENDEZVOUS which means that zero messages can be stored in the channel, and the sender is suspended until someone else calls receive (DefaultChannelCapacityIsZero.kt).

You can use a for-loop to receive messages from a channel which does not exit until the channel is closed (ReceivingForLoopExitsOnClose.kt).

Senders and receivers of a channel can be running in different scopes enabling parallel execution even if the sender is executed by a single thread context (ReceiverCanHaveOtherScope.kt).

Chapter 8 - Use cases

There is no need for starting a thread for each call to a remote endpoint (ServiceCaller.kt).

While not a true actor model (see Wikipedia), if you think of CompletableDeferred of a channel that only accepts a single message, it comes pretty close. The main function of this actor is to encapsulate mutable state (ActorWithInternalState.kt).

Actors do not have to send back messages to the caller, they can create new actors which do this for example to fetch data concurrently (ActorWithFanOut.kt).

Chapter 9 - Scopes and contexts

Launching a coroutine in a different dispatcher does not change the parent job. Exceptions will still cancel the coroutine it was launched from. Creating a new CoroutineScope will sever the job relation to the launching coroutine and allow the new coroutine to fail "silently" (ContextsDoNotChangeParentJob.kt).

A coroutine context can hold any number of elements of type CoroutineContext.Element such as Job, CoroutineDispatcher or CoroutineName which have special functions. But it can also hold custom elements (see CustomContextElement.kt). These can even be mutable, although this is probably a bad idea in most cases.