Explicit concurrency in Kotlin

I have asked this question on stackoverflow and it turns out to be more of a language design question.

I need to write a Kotlin suspending function which does the following:

  1. Launches a cancellable infinite operation, like collecting a flow or running a while cycle which generates flow values.
  2. After launching the operation it does some extra suspending checks, makes sure everything is fine and returns.
  3. The function should not suspend indefinitely while the operation is running, it should return to the caller once the operation is triggered and some extra preparations or checks are done.
  4. Besides the manual cancellation possibility, the operation must be automatically cancelled once the caller’s scope is cancelled.

I found couple of ways to satisfy all 4 points. For example, have a suspend function which takes a coroutine scope as an argument. Or use CoroutineScope(coroutineContext).launch in my function. However, both options were mentioned by @elizarov in his articles (this one and this one), and he wrote that we should not do that. He wrote that concurrent tasks should do one of the following:

  • Suspend until all the work is done. Which means using = coroutineScope { if some stuff needs to be done in parallel.
  • Fire and return immediately. Which means using non-suspending functions with coroutine scope parameter.

Should I always stick to these 2 cases in my function designs? To me (and not only me) it sounds like a valid use case which makes sense - do some suspending checks and launch the async op, later on returning to the caller the info that the checks passed successfully and the operation started.

Would be interesting to hear your thoughts on this.

1 Like

I suppose this is the question: Use coroutine scope in a kotlin suspend function - Stack Overflow and I already posted my comments there. As I said there, I don’t have a strong opinion about this and I’m interested in others opinion as well.

As a rule of thumb, I avoid creating functions that are both suspending and they accept a scope. If seeing such a function, I find it confusing, because it is like saying the function will wait for their work and it will run it in the background.

However, there is this corner case described by the OP, where we need to first wait for some initial preparations or checks and then run an operation in the background. In that case a suspending function accepting a scope feels right, but I think it should be clearly explained in the function docs. Alternatives I see:

  • Return immediately with a flow that passes operation states.
  • Return immediately with an object that has a function like: waitInitialized and optionally some props/functions for accessing the state of the background operation.

Both these solutions are more generic than suspend + scope, they allow to design an operation that consists of multiple stages and we can wait for any of these stages. Suspend + scope can only be used for exactly two stages of work.

I don’t like Broot’s suggestion of returning an object that you call waitInitialized() on because it’s too easy to forget to do that. An alternative that might work is to return an object with a start() method.

fun createThingThatDoesSomething(scope: CoroutineScope): ThingThatDoesSomething
suspend fun ThingThatDoesSomething.start()

Then you have one non-suspending function that takes a scope, and a suspending function that doesn’t. But it’s complicated, so I’m not sure I like it that much.

Having a suspend function that takes a scope breaks the rules, but rules exist so that you think before you break them. I’d probably go with that and document it.

1 Like