• Advanced Kotlin Coroutines tips and tricks
  • Written by Alex Saveau
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: nanjingboy
  • Proofread by: ZX-Zhu

Learn about barriers and how to get around them

Coroutines are stable from 1.3!

Getting started with Kotlin coroutines is pretty simple: Just put some time-consuming actions in launch, and you did that, right? Of course, this is for the simple case. But soon, the complexity of concurrency and parallelism builds up.

Here are some things you need to know as you delve into coroutines.

Cancel + block = 😈

There’s no way around it: at some point, you’ll have to use native Java streams. The problem here (😉 in many cases) is that using the stream will block the current thread. This is bad news in coroutines. Now, if you want to cancel a coroutine, you have to wait for the read and write operation to complete before you can continue.

As a simple repeatable example, let’s open ServerSocket and wait for a 1-second timeout connection:

RunBlocking (dispatchers.io) {withTimeout(1000) {val socket = ServerSocket(42) // We will be stuck here until someone receives the connection. Don't you want to know why? 😜 socket. The accept ()}}Copy the code

It should work, right? Not at all.

Now you feel something like: 😖. So how do we solve this?

When the Closeable APIs are well built, they support closing the stream from any thread and fail appropriately.

Note: In general, APIs in the JDK follow these best practices, but be aware that third-party Closeable APIs may not. You’ve been warned.

Thanks to suspendCancellableCoroutine function, when a collaborators cheng has been cancelled, we can turn off any flow:

public suspendinline fun <T : Closeable? , R> T.useCancellably( crossinline block: (T) -> R ): R =suspendCancellableCoroutine { cont -> cont.invokeOnCancellation { this? .close() } cont.resume(use(block)) }Copy the code

Make sure this applies to the API you are using!

Now the blocked accept call is wrapped with the useCancellably relaxed, and the coroutine will fail if the timeout is triggered.

RunBlocking (dispatchers.io) {withTimeout(1000) {val socket = ServerSocket(42) // Throws' SocketException: The socket closed 'is abnormal. Great! socket.useCancellably { it.accept() } } }Copy the code

Success!

What if you don’t support cancellation? Here’s what to look out for:

  • If you use coroutines to encapsulate any property or method in a class, there will be leaks even if you cancel coroutines. If you think you areonDestroyIt is especially important to clean up resources.Solutions:Move the coroutine toViewModelOr some other context-free class and subscribe to its processing results.
  • Make sure you useDispatchers.IOTo handle blocking operations, because this allows Kotlin to set aside threads for infinite waiting.
  • Use whenever possiblesuspendCancellableCoroutinereplacesuspendCoroutine.

launch vs. async

Since the above answers about these two features are out of date, I thought I’d analyze the differences again.

launchAbnormal bubble

When a coroutine crashes, its parent node is canceled, thus canceling all the children of the parent node. Once the coroutine cancels throughout the tree node, the exception is sent to the exception handler for the current thread. In Android, this means that your program will crash regardless of what you use for scheduling.

asyncHold your own exceptions

All this means that await () explicit handling exceptions, install CoroutineExceptionHandler will have no effect.

launch“Blocks” parent scope

Although this function returns immediately, its parent scope will not end until all coroutines built with Launch have been completed in some way. So if you just want to wait for all coroutines to complete, there is no need to call join() on all child jobs at the end of the parent scope.

Contrary to what you might expect, the outer scope will wait for the async coroutine to complete even if no await () is called.

asyncThe return value

This part is fairly simple: Async is the only option if you need the return value of a coroutine. If you don’t need a return value, use Launch to create side effects. And you need to complete these side effects before you can continue with join().

join() vs. await()

Join () does not rethrow an exception while await(). But if an error occurs, join() cancels your coroutine, which means that any code called after join() is suspended won’t work.

Record abnormal

Now that you know the difference between the different constructor exception handling mechanisms you use, you’re caught in a dilemma: you want to log exceptions without crashing (so we can’t use launch), but you don’t want to call a try/catch manually (so we can’t use async). So this leaves us at a loss? Thank goodness.

Record abnormal is the place where CoroutineExceptionHandler come in handy. But first, let’s take a moment to understand what happens when an exception is thrown in a coroutine:

  1. Catch the exception and passContinuationRecovery.
  2. If your code does not handle an exception and the exception is notCancellationException, then will pass the currentCoroutineContextRequest the firstCoroutineExceptionHandler.
  3. If the handler is not found or there is an error in the handler, the exception is sent to specific code in the platform.
  4. On the JVM,ServiceLoaderUsed to locate the global handler.
  5. Once all handlers are called or an error occurs in one handler, the exception handler for the current thread is called.
  6. If the current thread does not handle the exception, it bubbles up to the thread group and eventually to the default exception handler.
  7. Crash!

With that in mind, we have the following options:

  • Install one handler per thread, but this is not practical.
  • Install the default handler, but errors in the main thread won’t crash your application, and you’ll be in a potentially bad state.
  • Add handlers as servicesWhen usinglaunchHacky is called when any coroutine of
  • Use your own custom fields and additional handlers insteadGlobalScope, or add handlers to every scope you use, but this is annoying and makes logging optional instead of default.

This last option is recommended because it is flexible and requires minimal code and skill.

For application-wide jobs, you will use AppScope with a logging handler. For other businesses, you can add handlers at the appropriate locations for logging crashes.

val LoggingExceptionHandler = CoroutineExceptionHandler { _, t ->
    Crashlytics.logException(t)
}
val AppScope = GlobalScope + LoggingExceptionHandler
Copy the code
class ViewModelBase : ViewModel(), CoroutineScope {
    override val coroutineContext = Job() + LoggingExceptionHandler

    override fun onCleared() = coroutineContext.cancel()
}
Copy the code

It’s not so bad

Final thoughts

Any time we have to deal with an edge situation, things tend to get messy very quickly. I hope this article has helped you understand the various problems you may encounter in non-standard conditions and the solutions you can use.

Happy Kotlining!

  • Alex Saveau (@SUPERCILEX) | Twitter: Latest Tweets from Alex Saveau (@supercilex). All things 🔥 Base, Android, and open source. Also, 🐤 builds…

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.