Search code examples
kotlinkotlinx.coroutines

Kotlin 1.3: how to execute a block on a separate thread?


I've been reading up about concurrency in Kotlin and thought I started to understand it... Then I discovered that async() has been deprecated in 1.3 and I'm back to the start.

Here's what I'd like to do: create a thread (and it does have to be a thread rather than a managed pool, unfortunately), and then be able to execute async blocks on that thread, and return Deferred instances that will let me use .await().

What is the recommended way to do this in Kotlin?


Solution

  • 1. Single-threaded coroutine dispatcher

    Here's what I'd like to do: create a thread (and it does have to be a thread rather than a managed pool, unfortunately)

    Starting a raw thread to handle your coroutines is an option only if you're prepared to dive deep and implement your own coroutine dispatcher for that case. Kotlin offers support for your requirement via a single-threaded executor service wrapped into a dispatcher. Note that this still leaves you with almost complete control over how you start the thread, if you use the overload that takes a thread factory:

    val threadPool = Executors.newSingleThreadExecutor {
        task -> Thread(task, "my-background-thread")
    }.asCoroutineDispatcher()
    

    2. async-await vs. withContext

    and then be able to execute async blocks on that thread, and return Deferred instances that will let me use .await().

    Make sure you actually need async-await, which means you need it for something else than

    val result = async(singleThread) { blockingCal() }.await()
    

    Use async-await only if you need to launch a background task, do some more stuff on the calling thread, and only then await() on it.

    Most users new to coroutines latch onto this mechanism due to its familiarity from other languages and use it for plain sequential code like above, but avoiding the pitfall of blocking the UI thread. Kotlin has a "sequential by default" philosophy which means you should instead use

    val result = withContext(singleThread) { blockingCall() }
    

    This doesn't launch a new coroutine in the background thread, but transfers the execution of the current coroutine onto it and back when it's done.

    3. Deprecated top-level async

    Then I discovered that async() has been deprecated in 1.3

    Spawning free-running background tasks is a generally unsound practice because it doesn't behave well in the case of errors or even just unusual patterns of execution. Your calling method may return or fail without awaiting on its result, but the background task will go on. If the application repeatedly re-enters the code that spawns the background task, your singleThread executor's queue will grow without bound. All these tasks will run without a purpose because their requestor is long gone.

    This is why Kotlin has deprecated top-level coroutine builders and now you must explicitly qualify them with a coroutine scope whose lifetime you must define according to your use case. When the scope's lifetime runs out, it will automatically cancel all the coroutines spawned within it.

    On the example of Android this would amount to binding the coroutine scope to the lifetime of an Activity, as explained in the KDoc of CoroutineScope.