Structured Concurrency Anniversary

Roman Elizarov
5 min readSep 28, 2019

--

Photo by Annie Spratt on Unsplash

A little over a year ago I announced big conceptual shift in the design of Kotlin Coroutines called Structured Concurrency. From that moment on, it took our team about a month to make the first stable 1.0.0 release of kotlinx.coroutines library. After a year of further work, kotlinx.coroutines had added stable support for cold flows that integrate nicely with reactive streams. The library had reached version 1.3.2 by now. It is good time to look back and see how it all worked out — what was great, what could be improved.

Structured Concurrency accomplished more than we hoped for. Originally, the design of structured concurrency was based on the woes experienced by backend developers trying to implement all sorts of asynchronous and concurrent logic. It was focused on making sure that you cannot ever lose a running coroutine or an exception. The key building block we added back then is coroutineScope { ... } function, which encapsulates concurrent operations and limits their scope to the scope of the current call.

There was not much else to it, so our recommendation to UI developers was to implement CoroutineScope interface in various “closeable” entities of their applications. We envisioned a simple picture with a simple scope hierarchy. It turned out to be more elaborate in practice.

Structured Concurrency was rapidly adopted by Android, which has quite complicated life-cycles. Android libraries added extensions like lifecycleScope and viewModelScope, enabling concise and safe integration of coroutines with those concepts. It became apparent that code looks clearer when an object encapsulating the scope is separate from the rest of code. Introductory Android Codelab on Coroutines recommends defining coroutine scope like this:

private val scope = CoroutineScope(...)

Nowadays, this style increasingly looks more appealing, so it’s time to adjust our documentation to reflect it.

At the same time, structured concurrency laid a solid foundation for using Kotlin Flows in UI applications. With a dedicated launchIn operator we can observe events from a flow in a given scope using the following code:

flow.onEach { ... }
.launchIn(scope)

It is a perfect use-case for structured concurrency. You’d design this kind of API if you were working on a dedicated reactive framework for UI where you want to ensure that no subscription is left dangling when widgets close.

Walk down memory lane

I recently gave a talk on Hydra Conference, which focuses on distributed systems and computing, where I told the original story behind the design of Kotlin Coroutines and how we had discovered the concept of Structured Concurrency and found a name for it (you can watch the recording or scroll though the slides).

To prepare for that talk I’ve actually looked through the early prototypes of what had later become kotlinx.coroutines library. It is curious enough that the original prototype name for the Job interface was Lifetime. In pre-release versions before structured concurrency you’d have to interact with Job methods quite often to manage concurrency in your application. Structured concurrency made most of it just a low-level implementation detail. You don’t deal with Job directly so often now when you have a ready-to-use implementation of CoroutineScope that encapsulates an instance of a job in its context.

Legacy

We are still haunted by some of the pre-release legacy. Look at the signature of the most basic coroutine builder (or any other coroutine builder):

fun CoroutineScope.launch(context: CoroutineContext, ...): Job

As explained in the story on coroutine context and scope, it combines the context from the scope and the explicitly passed context parameter to produce a child context. This is very convenient feature, since you can do things like:

scope.launch(Dispatchers.IO) { .... }

Instead of a longer code with essentially the same effect:


scope.launch {
withContext(Dispatchers.IO) { ... }
}

However, if you pass a context parameter with another Job element inside of it, the resulting behavior of the code, while well specified, may be unintuitive. Have we designed it from scratch, we could have forbidden the job in the context argument, thus making sure that it can only come from the scope. That would have completely avoided the whole question of how it should behave by default when there are two jobs.

Yet, we had to support gradual migration of all the code our early adopters wrote before the structured concurrency, so we did not have this option. We don’t recommend passing jobs in the context parameter to coroutine builders in modern code, though. Moreover, the corresponding context-management feature of Kotlin Flow (flowOn operator) was designed with this lesson in mind, so it does not support swapping job in its execution context and does not have any surprising behaviors.

Road ahead

The goal of library design is to give application developers ready-to-use tools that cover their most frequent use-cases and solve the most popular problems. If something is needed all the time by any kind of application, it should be simple and straightforward to code. The correct code shall be the easiest one to write, while advanced, rarely needed corner-cases can and should take longer. We have the essentials covered with kotlinx.coroutines library.

However, there are lots of directions where structured concurrency can be further taken to. As adoption grows, there are advanced patterns of code that nonetheless start to occur quite often. When they are hard or error-prone to write properly, it is a good indicator that we might need to introduce additional features to help. Like that problem with two jobs in the context I’ve mentioned before. In fact, there are valid use-cases where you have an entity with its own life-time, like a HTTP connection, and a client code in another module of your application making a request in its own scope. You should be able to easily combine them so that request is restricted to both life-times. Currently, this is cumbersome to implement.

There are more things like that to keep us busy working on improvements.

Conclusion

Structured Concurrency in Kotlin Coroutines, being just over a year old, turned out to be a nice and cute kid. It has lots of potential and an amazing and promising future, helping developers in various domains to manage lifetime of their concurrent primitives

--

--

Roman Elizarov

Project Lead for the Kotlin Programming Language @JetBrains