I have a Kotlin application where we are unable to use runBlocking() from an app entry point (like main). I've found that I can do the following to do a fire-and-forget asynchronous operation, but I'm unclear on whether this is a good practice -- I see this in the docs:
Do not replace GlobalScope.launch { ... } with CoroutineScope().launch { ... } constructor function call. The latter has the same pitfalls as GlobalScope.
But I've also seen places where people seem to be recommending this approach. Here's an example of the approach:
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
val scope = CoroutineScope(Dispatchers.IO)
fun doStuff(): String {
val response = "all done"
println("before fireAndForget")
fireAndForget()
println("after fireAndForget")
return response
}
fun fireAndForget() {
scope.launch {
makeSomeExpensiveCall()
}
}
fun makeSomeExpensiveCall() {
// In reality, make a blocking database call, etc. without coroutines, reactive, etc.
Thread.sleep(100)
println("after sleep")
}
fun main() {
val response = doStuff()
Thread.sleep(200)
println(response)
}
The above prints the following, which is what I expect. I just don't know if it's safe to do this.
before fireAndForget
after fireAndForget
after sleep
all done
EDIT:
The below is a bit more realistic version of what I'm trying to do. I'm still unsure of whether the scope is being set up and used correctly.
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class Controller {
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun saveDataToDatabase(): String {
saveRecord()
println("before saveAuditDataWithoutBlocking")
saveAuditDataWithoutBlocking()
println("after saveAuditDataWithoutBlocking")
return "Save succeeded"
}
private fun saveRecord() {
Thread.sleep(10)
}
private fun saveAuditDataWithoutBlocking() {
// Write more data to a database, but do not block caller
val job = scope.launch {
saveAuditInfoBlocking()
}
job.invokeOnCompletion { throwable ->
throwable?.let {
println("Exception in fireAndForget: $throwable")
}
}
// Is there something else I need to do?
}
private fun saveAuditInfoBlocking() {
Thread.sleep(100)
println("Finished slow write of audit data")
}
}
fun main() {
val controller = Controller()
println("Calling saveDataToDatabase()")
val response = controller.saveDataToDatabase()
println("Received response in main(): $response")
Thread.sleep(200)
println("main() finished")
}
Output:
Calling saveDataToDatabase()
before saveAuditDataWithoutBlocking
after saveAuditDataWithoutBlocking
Received response in main(): Save succeeded
Finished slow write of audit data
main() finished
GlobalScope's documentation says the purpose for which it is OK to use:
The reason you usually want to avoid it is that in most applications, almost everything has some scope to it that is limited. For instance, if you are doing some action on some screen of your app, you want to cancel that action when you move to a different screen so you aren't leaking resources.
But if you are doing something that should last for the lifetime for the app, such that you can't possibly be hanging onto some resource in your coroutine for longer than you want to, then GlobalScope is appropriate.
You have to be really careful though. Consider this psuedo-code:
This is a task that you never want to be cancelled because you want to keep downloading the file even if the user navigates away from this screen, so you think, this is a job for GlobalScope! But the problem is that you have referenced a screen element
viewLayoutin your coroutine, so a reference to the entire layout of this screen is captured by the coroutine that will outlive your screen. This is a memory leak.Since it is so easy to create memory leaks with GlobalScope, they annotate it with
@DelicateCoroutinesApito discourage its use. So then, some naïve users create a one-off CoroutineScope to run a single coroutine (i.e.CoroutineScope().launch { ... }). This avoids the warning from@DelicateCoroutinesApiwithout changing the behavior it's warning you about.Incidentally, the way to fix the code above would be to move the first line out of the coroutine so the
urlalone is captured by the coroutine.The code you quoted is a little weird. They have created a CoroutineScope that:
SupervisorScope()would be more appropriate.cancel()it when its associated scope is destroyed. GlobalScope can be used instead for less convoluted code unless you truly want the behavior mentioned in the point above.Since it shows some made-up pointless task that doesn't hang onto resources, it doesn't really matter that it's never cancelled, but it could just as well have been a GlobalScope.
The documentation you quoted is talking about creating a CoroutineScope with the constructor call
CoroutineScope(), and immediately callinglaunchon it without first storing it in a property so you can cancel it at the appropriate time. This code example is not exactly what they're talking about, although it's similar since they create a CoroutineScope and then never callcancel()on it.