Short(-ish) story
I wonder if there is more or less standard way of creating a coroutine context/scope such that:
- It is a child of a current coroutine for structured concurrency,
- It can be stored in some property, etc. and later be used for running asynchronous tasks with e.g.
launch().
coroutineScope() does exactly what I need, it creates a child scope, but it does not simply return it to the caller - we need to pass a lambda and coroutine lifetime is limited to the execution of this lambda. On the other hand, CoroutineScope() factory creates a long-running scope that I can store for a later use, but it is unrelated to the current coroutine.
I was able to create such a scope manually with:
suspend fun createChildCoroutineScope(): CoroutineScope {
val ctx = coroutineContext
return CoroutineScope(ctx + Job(ctx.job))
}
On first sight it seems to do exactly what I need. Is it an equivalent of what coroutineScope() does or maybe my solution is somehow incomplete and I should perform some additional tasks? I tried to read the source code of coroutineScope(), but it is quite complicated. Is there a simpler or more standard way of just creating a child scope?
Also, is it considered a bad practice or anti-pattern? I'm just concerned that if there is no such a simple function already, then maybe it is so for a reason and I shouldn't really use coroutines this way.
Use cases (longer story)
Usually, I see a need for this when I'm implementing some kind of a long-running service that could asynchronously schedule background operations:
class MyService {
fun scheduleSomeTask() {
// start task in the background
// return immediately
}
}
There are several possibilities to do this with coroutines:
GlobalScope, but it is bad.Make
scheduleSomeTask()suspendable and run background tasks using current coroutine. In many cases I think this approach isn't really a proper one:- Background tasks are "owned" by the caller and not by the service itself. If we e.g. stop the service, the background task will be still running.
- It requires that the scheduling function is suspendable. I think this is wrong, because I don't really see a reason why some Java code or a code outside of coroutines context should not be allowed to schedule tasks in my service.
Give my service a defined lifecycle, create scope with
CoroutineScope()andcancel()it when stopping/destroying. This is fine, but I think we could still benefit from coroutines's structured concurrency, so for me it is a disadvantage that my service is detached.For example, we have a file downloading service and it consists of (owns) other services, including a data caching service. With typical approach of
start()/stop()services we need to control lifecycle manually and it is hard to properly handle failures. Coroutines make it much easier: if caching service crashes, it automatically propagates to downloading service; and if downloading service needs to stop, it just cancels its coroutine and it can be sure it doesn't leak any of its subcomponents. So for me structured concurrency of coroutines could be really useful when designing an application consisting of several small services.
My current approach is something like:
class MyService {
private lateinit var coroutine : CoroutineScope
suspend fun start() {
coroutine = createChildCoroutineScope() + CoroutineName("MyService")
}
fun stop() {
coroutine.cancel()
}
fun scheduleSomeTask() {
coroutine.launch {
// do something
}
}
}
Or, alternatively:
class MyService(
private val coroutine: CoroutineScope
) {
companion object {
suspend fun start() = MyService(createChildCoroutineScope())
}
}
This way the service "intercepts" the coroutine that started it and attaches its background operations to it. But as I said, I'm not sure if this isn't considered an anti-pattern for some reason.
Also, I understand my createChildCoroutineScope() is potentially dangerous. By calling it we make current coroutine uncompletable. This might be the reason why such a function does not exist in the library. On the other hand, it is not really different than doing something like:
launch {
while (true) {
socket.accept() // assume it is suspendable, not blocking
// launch connection handler
}
}
In fact, both approaches are very similar from technical point of view. They have similar concurrency structure, but I believe "my" approach is often cleaner and more powerful.