This project explores the possibilities and problems of coroutines via ready to run examples.
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).
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).
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.
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).
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).
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).
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).
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).
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.