Spin up child coroutine without blocking parent completion


#1

Wondering if there’s something I’m missing here (or if this is the wrong approach):

  • I have a core class whose methods can be called from many threads. Currently, I mark the methods as synchronized to make sure things are thread-safe.
  • This core class manages some things beneath it. Mostly they are short, synchronous code paths but there is some logic which schedules some recurring tasks (some blocking, some non-blocking).
  • I wanted to try out using coroutines here to:
    1. Have the core class execute all its logic within a single coroutine context (served by a single thread) to take care of the thread safety and avoid spinning up extra threads for the non-blocking, recurring tasks
    2. Schedule the non-blocking, recurring tasks in that same coroutine context
    3. Schedule the blocking, recurring tasks in a separate coroutine context, served by multiple threads

I have a short experiment with an aspect of this (everything except #3 above) pasted below:

val mainContext = newSingleThreadContext("main")

fun log(msg: String) {
    println("[${Thread.currentThread().name}] $msg")
}

suspend fun doNonblockingSomething(): Boolean {
    // Launch a recurring, non-blocking task that will be executed
    // in the same context
    launch (coroutineContext) {
        repeat(5) {
            log("loop #$it")
            delay(1000)
        }
    }
    log("end of doNonblockingSomething")
    return true
}

class CoreClass {
    // Transition from whatever the calling context is to run
    // all code from here onward in 'mainContext'
    suspend fun doThing(): Boolean = withContext(mainContext) {
        doNonblockingSomething()
    }
}

fun main(args: Array<String>) {
    val core = CoreClass()
    log("launching task")
    // Bridge blocking code to coroutine code
    val result = runBlocking {
        val x = core.doThing()
        log("got result $x")
        x
    }
    log("main got result $result")
}

It outputs:

[main] launching task
[main @coroutine#1] end of doNonblockingSomething
[main @coroutine#2] loop #0
[main @coroutine#1] got result true
[main @coroutine#2] loop #1
[main @coroutine#2] loop #2
[main @coroutine#2] loop #3
[main @coroutine#2] loop #4
[main] main got result true

What surprised me was that the main runBlocking block doesn’t finish until the coroutine launched inside doNonblockingSomething finishes as well, even though doNonblockingSomething returns. I suppose this is because blocks like runBlocking wait until all coroutines (including any children) are finished. So what I’m wondering is: how can I spin off a child coroutine, executed in the current context, without blocking the parent one from the perspective of things like runBlocking?


#2

I ended up making this work by creating a Job object and setting that as the parent of the ‘background’ coroutine, like so:

suspend fun doNonblockingSomething(): Boolean {
    val bgJob = Job()
    // Launch a recurring, non-blocking task that will be executed
    // in the same context
    launch (coroutineContext, parent = bgJob) {
        repeat(5) {
            log("loop #$it")
            delay(1000)
        }
    }
    log("end of doNonblockingSomething")
    return true
}

Now the call finishes with the result and the loop continues in the background.