frangulyan
frangulyan

Reputation: 3877

Use coroutine scope in a kotlin suspend function

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 was not able to find a way to implement this without passing the coroutine scope to this function as an extra parameter or without making this function an extension on CoroutineScope:

suspend fun start(scope: CoroutineScope): Boolean {
    // start an infinite operation until stopped or cancelled
    job = scope.launch {
        while (isActive) {
            // ... do something useful
            delay(5000)
        }
    }
    
    // do some extra preparations
    doSomeSuspendingPreparations()

    // tell the caller that the operation started successfully
    return true
}

suspend fun stop() {
    job.cancelAndJoin()
}

My question is: can I implement this function without passing it a scope, letting it somehow use the calling scope, or is this the only way?

Upvotes: 1

Views: 1054

Answers (1)

frangulyan
frangulyan

Reputation: 3877

As @broot pointed out, what I want is supported, but discouraged in Kotlin. Here is the link to his answer. Also a very nice article on this topic by one of the Kotlin creators.

As the article states, what I want is possible:

suspend fun doNotDoThis() {
    CoroutineScope(coroutineContext).launch {
        println("I'm confused")
    }
    doSomeExtraSuspendingStuff()
}

We can just create a new scope using current suspending function's context - coroutineContext - and launch a coroutine which runs concurrently with the rest of the code.

This is, however, not fitting the Explicit Concurrency mindset of Kotlin. A Kotlin function should either fire some task and return immediately, or do its job and wait until everything is finished. My example wants to mix both worlds, partially do something concurrently and partially do something suspending.

So, for explicitly stating our concurrency purpose, our functions should do one of these two:

  • Suspend until all the work is done and return after that. This is done by simply declaring a normal suspend function. If we need to have some concurrent executions inside it, we can use coroutineScope {} builder. This builder, however, will suspend until all its children are finished:

      suspend fun doSomething() = coroutineScope {
          launch {
              doSomeConcurrentWork()
          }
          launch {
              doSomeOtherConcurrentWork()
          }
          // this will return only after  both concurrent works are finished
      }
    
  • Fire and return immediately. This is done by creating a regular non-suspending extension on a CoroutineScope or taking a CoroutineScope as a parameter:

      fun CoroutineScope.doThis() {
          launch { println("I'm fine") }
      }
    
      fun doThatIn(scope: CoroutineScope) {
          scope.launch { println("I'm fine, too") }
      }
    
      fun main() = runBlocking<Unit> {
          doThis()
          doThatIn(this)
      }
    

With this convention the function signature is being explicit about its concurrency approach, we can just look at the function and know how it will be run relative to the rest of our code.

Upvotes: 2

Related Questions