Introducing kotlinx.coroutines 1.6.0
Following the release of Kotlin 1.6.0, the 1.6.0 version of the
kotlinx.coroutines library is out. Here are the main features it brings:
- A new API and multiplatform support for
kotlinx-coroutines-testintroduce a common solution for writing portable tests with suspending functions.
- Support for the new Kotlin/Native memory manager makes multithreaded coroutines for Kotlin/Native available out of the box.
- The new dispatcher views API lets you limit parallelism without creating additional thread pools and provides a unified way to create dispatchers for unbound parallelism.
- The introduction of
CopyableThreadContextElementgives coroutines thread-safe write access to
- Migration to the Java 8 target: we’ve taken the first step of this migration by compiling against Java 8 source and binary targets.
In this blog post, we’ll take a closer look at all the new features. To try them out right away, jump to the How to try it section.
A new API and multiplatform support for
Following our roadmap, we completely reworked
kotlinx-coroutines-test. The testing module received multiplatform support and solved the problem of writing portable tests with suspending functions, which we decided to shift into the library space. The new experimental API also fixed multiple issues with the previously used
The entry point to the new API is the
runTest() function, which you can use on any platform to test code that involves coroutines.
runTest() will automatically skip calls to
delay() and handle uncaught exceptions. Unlike
runBlockingTest(), it will wait for asynchronous callbacks to handle situations where some code runs in dispatchers that are not integrated with the test module.
runTest()only once per test and immediately return its result. This restriction is necessary for the test to work on the JS platform. If for some reason you need to call
runTest()several times, please share your use case using the issue tracker.
Support for the new Kotlin/Native memory manager
The GitHub issue about supporting multithreaded coroutines for Kotlin/Native has received a huge number of upvotes. In 1.3.2, we started maintaining a companion library version that included the feature’s experimental implementation within the regular Kotlin/Native memory model. Since then, we have started publishing a separate
native-mt artifact for each release to share this implementation.
With Kotlin 1.6.0, we announced the new experimental Kotlin/Native memory management scheme, which made the limitations of the existing
native-mt version possible to overcome. In this release, we implemented support for the new memory manager and merged the implementation into the mainline. This means you only need the 1.6.0 version of
kotlinx.coroutines to try experimental Kotlin/Native multithreading with the new memory model.
Since the old
native-mt implementation still suffers from memory leaks in some concurrent execution scenarios, we are going to decommission it starting with
kotlinx.coroutines 1.7.0. For the 1.6.* releases,
native-mt artifacts will still be published. You can migrate to the new memory management scheme by following the migration guide.
Dispatcher views API
You might want to control concurrency while using coroutines, for example, to limit the number of concurrent requests to a database. A popular solution for this is to define a custom coroutine dispatcher with the newFixedThreadPoolContext() function which is then used on every database invocation:
Unfortunately, this approach has several problems:
newFixedThreadPoolContext()can create many unnecessary threads, most of which are idle, consuming the memory, CPU, and device battery.
withContext(DB)invocation performs an actual switch to a different thread, which can be extremely resource intensive.
- The result of
newFixedThreadPoolContext()needs to be explicitly closed when no longer used. This is quite error-prone, often forgotten about, and can lead to leaking threads.
- If you have several thread pools as separate executors, they cannot share threads and resources.
kotlinx.coroutines 1.6.0, we’ve introduced the new dispatcher views API as an option to limit concurrency without creating additional threads and allocating extra resources. To start using dispatcher views, just call
limitedParallelism() instead of
The new approach addresses the limitations of using custom thread pools:
- A dispatcher view is only a wrapper to the original dispatcher. Using the original dispatcher’s resources, it limits the number of coroutines that can be executed simultaneously and doesn’t create new threads.
Dispatchers.IO, or their views attempts not to switch threads when possible.
- A view doesn’t need to be closed.
- To create separate executors, you can take multiple views of the same dispatcher and they will share threads and resources. There is no limit on the total parallelism value, but the effective parallelism of all views cannot exceed the actual parallelism of the original dispatcher. This means that you can control the parallelism of both the entire application and each view separately.
Dispatchers.IO elasticity for limited parallelism
Imagine a case where your application uses multiple views as separate executors and needs each one to guarantee a specified level of parallelism during peak loads. You don’t need to create a parent dispatcher with the desired total parallelism value. Instead, you can use views of
Dispatchers.IO which can create and shutdown additional threads on-demand, saving resources in a steady state.
The implementation of
Dispatchers.IO is elastic. This means that
Dispatchers.IO itself still has a limit of 64 threads, but each of its views will have the effective parallelism of the requested value.
In the example:
- During peak loads, the system may have up to 64 + 80 + 40 threads dedicated to blocking tasks.
- In a steady state, there will be only a small number of threads shared among
Under the hood, it works with an additional dispatcher backed by an unlimited pool of threads. Both
Dispatchers.IO and its views are actually views of that dispatcher and share threads and resources.
In Java, you can use a
ThreadLocal variable to maintain some value related to the current thread. In
kotlinx.coroutines, the same can be achieved with
ThreadContextElement. However, since
ThreadContextElement is a part of
CoroutineContext, it gets inherited by child coroutines, which can execute concurrently. This can be a problem if the underlying value is not thread-safe and gets concurrently mutated, for example, when implementing logging contexts or tracing frameworks.
To resolve the issue, we created the new
CopyableThreadContextElement interface, which defines an extra
copyForChildCoroutine() method. During the child coroutine’s launch time, the method will be called on the parent
ThreadContextElement instance to obtain a new
ThreadContextElement for the child coroutine’s context. Thus, every coroutine will have its own element copy that it can mutate, guaranteeing thread safety for the underlying value.
Migration to the Java 8 target
Maintaining support for the Java 6 target requires a lot of work and prevents us from using potentially helpful features that Java 8 offers. Kotlin already migrated to Java 8 in version 1.5.0. Catching up with the language,
kotlinx.coroutines 1.6.0 has begun the migration process by compiling against the Java 8 source and binary targets.
Although support for the Java 6 target has been dropped,
kotlinx.coroutines 1.6.0 remains compatible with popular Android API levels and uses new features of Java 7+ only if there is a way to provide a proper fallback.
How to try it
kotlinx.coroutines 1.6.0 is available from Maven Central. To try it in your project, do the following:
- Make sure that you have
mavenCentral()in the list of repositories:
- Make sure that you are using the latest version of Kotlin:
kotlinx.coroutinesas a dependency:
If you run into any trouble
- Report issues to the GitHub issue tracker.
- Look for help in the #coroutines channel on the Kotlin Slack (get an invite).