I've got an app that only uses coroutines in one s...
# coroutines
b
I've got an app that only uses coroutines in one spot (where I'm using ktor) and I want to fire-and-forget a set of requests (I don't want to block the caller on the responses, I just log when responses are received or when they time out). I have an existing IO thread pool for tasks like this. I was originally doing (simplified):
Copy code
fun my update() = runBlocking {
    subscribers.forEach { url -> 
        launch(myIoPool.asCoroutineDispatcher()) {
            try {
                val resp = client.postJson(...)
                <http://logger.info|logger.info>("Got response...")
            } catch (e: HttpRequestTimeoutException) {
                logger.error("Timed out...")
            }            
        }
}
But I've now realized this is blocking until all the calls to
launch
finish. What's the best way to fire-and-forget here? I think I've seen I can do this with
GlobalScope.launch
(and getting rid of the
runBlocking
), but from what I've read it seems like it's rarely a good idea to use that.
a
GlobalScope.launch
does basically what you need here - the scope is intended to cancel things that are still ongoing when the scope ends. You might consider that if your app has “start” and “stop” concepts, then that defines a scope which would be more precise than GlobalScope.
b
Ok, yeah from what I read the concern with
GlobalScope
is the leaking, but this particular task basically runs for the lifetime of the app, so maybe it's not a worry?
If this class had its own start and stop methods, what would be the way to do that? Have the class create its own scope to use instead of
GlobalScope
?
a
Yes, you can just call
CoroutineScope()
to create a new standalone scope (usually just a property initialiser) and then
scope.cancel()
in your stop/close method
My own experience is that launching things without allowing for housekeeping to clean them up often comes back to bite eventually.
☝️ 1
b
Ok, I'll play with that. Thanks Steve!
g
rarely a good idea to use that
Yes, specifically because it can be used in cases as yours, global fire and forget background tasks Replacing GlobalScope with own scope can be better, but only if you really have some code which cancels it, otherwise it would be exactly the same
b
Ok good to know. I don't ever cancel this (as of now), but using a custom scope with a hook to pass something else in the constructor worked out well since I've got a good hook for tests.
g
I think such code as yours should be a bit improved, to be sctructured concurrency friendly and more flexible as for end user and for usages in tests:
Copy code
suspend fun myUpdate(dispatcher: CoroutineDispatcher = myIoPool.asCoroutineDispatcher()) {
    coroutineScope {
        subscribers.forEach {
            launch(dispatcher) {
                try {
                    val resp = client.postJson(...)
                    <http://logger.info|logger.info>("Got response...")
                } catch (e: HttpRequestTimeoutException) {
                    logger.error("Timed out...")
                }
            }
        }
    }
}
So now this operation is more correct it cancellable, follows structured concurrency and can be used in different contexts depending on case:
Copy code
fun myBlockingUpdate() = runBlocking {
    myUpdate()
}

fun myBackgroundUpdate(): Job {
    GlobalScope.launch {
        myUpdate()
    }
}

suspend fun mySuspendUpdate() {
    <http://logger.info|logger.info>("Before update")
    myUpdate()
    <http://logger.info|logger.info>("After update")
}
b
Thanks, Andrey. In the case of
myBackgroundUpdate
there, will there be two thread transitions? Calling thread -> GlobalScope thread -> io pool thread? Trying to make sure I'm understanding it correctly.
a
Looks like that suggestion assumes that
myUpdate()
is just a suspend function to call. You can do
GlobalScope.launch(myIoPool.asCoroutineDispatcher()) { }
to specify the scope and override coroutine context (i.e. the dispatcher here) in one go