mer. Mai 25th, 2022

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-test introduce 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 CopyableThreadContextElement gives coroutines thread-safe write access to ThreadLocal variables.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 kotlinx-coroutines-test

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 runBlockingTest() scheme.

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.

Call 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.

@Test
fun testF() = runTest { // Run a coroutine with virtual time
val actual = f()
val expected = 42
assertEquals(actual, expected)
}

suspend fun f(): Int {
delay(1_000) // Will finish immediately instead of delaying
return 42
}

You can find a detailed description of the API in the module’s README. To adapt the existing test code to the new scheme, please follow the migration guide. The old API has now been deprecated.

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:

class UserRepository {
private val DB = newFixedThreadPoolContext(10, « DB »)

suspend fun getUserById(userId: Int): User? = withContext(DB) {
executeQuery(« SELECT * FROM users WHERE id = $1 », userId).singleOrNull()
}
}

Unfortunately, this approach has several problems: 

newFixedThreadPoolContext() can create many unnecessary threads, most of which are idle, consuming the memory, CPU, and device battery.Every 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.

In 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 newFixedThreadPoolContext().

class UserRepository {
private val DB = Dispatchers.IO.limitedParallelism(10)

suspend fun getUserById(userId: Int): User? = withContext(DB) {
executeQuery(« SELECT * FROM users WHERE id = $1 », userId).singleOrNull()
}
}

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.The withContext() invocation with Dispatchers.Default, 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.

val backgroundDispatcher = newFixedThreadPoolContext(5, « App Background »)
// At most 2 threads will process images
val imageProcessingDispatcher = backgroundDispatcher.limitedParallelism(2)
// At most 1 thread will do IO
val fileWriterDispatcher = backgroundDispatcher.limitedParallelism(1)
// At most 3 threads will handle database requests
val dbDispatcher = backgroundDispatcher.limitedParallelism(3)

// At most 5 coroutines can be executed simultaneously

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 limitedParallelism() for 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. 

// 80 threads for PostgreSQL connection
val myPostgresqlDbDispatcher = Dispatchers.IO.limitedParallelism(80)
// 40 threads for MongoDB connection
val myMongoDbDispatcher = Dispatchers.IO.limitedParallelism(40)

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 Dispatchers.IO, myPostgresqlDbDispatcher, and myMongoDbDispatcher.

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.

Introduction of CopyableThreadContextElement

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:

repository {
mavenCentral()
}

Make sure that you are using the latest version of Kotlin:

plugins {
kotlin(« jvm ») version « 1.6.0 »
}

Add kotlinx.coroutines as a dependency:

dependencies {
implementation(« org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0 »)
}

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).

Read more

GitHub libraryAPI documentationKotlin 1.6.0 release blog post

By

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.