I have a function that wants to be able to spawn a...
# coroutines
s
I have a function that wants to be able to spawn a new coroutine via
launch
but don't want to dictate which CoroutineScope it is attached to, so I have created it as an extension method to
CoroutineScope
so
launch
can be called against it. This approach feels like it is guarded pretty heavily against through the use of
coroutineScope
and friends - is there a more idiomatic way I should be spawning new tasks in the context of the calling function, or is this approach considered okay?
🆗 1
hypothetically:
Copy code
fun CoroutineScope.runThing() {
  launch {
    delay(1000)
    println("hello")
  }
}
then somewhere else
Copy code
coroutineScope {
  runThing()
  // <snip>
}
c
That is considered good practice, yes.
👍 2
To explain why: this lets the caller decide how they should deal with cancellation, if they want to wait for the results, etc. If they want to fire and forget it, they can create a new scope from a job they control (so they can decide to cancel if they want to). If they want to wait for the results, they can use
coroutineScope
. Etc
s
Thanks, that aligns with my thinking. It just seemed to go against the grain of the topics surrounding Structured Concurrency.
b
I would advocate for opposite - making runThing suspend fun rather than extension of CoroutineScope,
To explain why: this lets the caller decide how they should deal with cancellation
Same possible with suspend runThing:
If they want to fire and forget it, they can create a new scope from a job they control (so they can decide to cancel if they want to).
They still can do this:
Copy code
newScope.launch { runThing() }
If they want to wait for the results, they can use 
coroutineScope
.
they just have to call
runThing
So my point is that if it's still possible to control the flow of the
runThing
fun doesn't make it good practice
👍 1
s
In my case, the return type implements the
Job
that is being launched. So it still allows for that case.
More specifically...
Copy code
data class RateLimit(private val channel: ReceiveChannel<Unit>, private val job: Job): Job by job {
  suspend operator fun <T> invoke(fn: suspend () -> T): T {
    channel.receive()
    return fn()
  }
}

fun CoroutineScope.rateLimit(
  count: Int,
  per: Duration,
  burst: Int = 1
): RateLimit {
  require(count > 0)
  require(!per.isNegative && !per.isZero)
  require(burst > 0)

  val channel = Channel<Unit>(burst)

  val job = launch {
    while (isActive) {
      val before = Instant.now()
      repeat(count) {
        channel.send(Unit)
      }
      val after = Instant.now()
      val elapsed = Duration.between(before, after).abs()
      delay(per - elapsed)
    }
  }

  job.invokeOnCompletion { channel.close(it) }

  return RateLimit(channel, job)
}
@bezrukov would you still suggest removing the
launch
from the above code and place that responsibility on the call site?
b
no, most likely it OK for your usecase
s
It would be nice (perhaps?) for suspended functions to be able to call `launch`/`async` and implicitly attach to the calling function's
CoroutineScope
b
you can do it indirectly via
Copy code
CoroutineScope(currentCoroutineContext()).launch { 
            
}
but I don't think it's good practice too
c
@bezrukov the conventions say to use suspend for functions that don't create sub tasks, and to use a CoroutineScope receiver for functions that do.
@Scott Christopher it wouldn't: the goal is to tell the caller what happens if they call your function. If you make it
suspend
, the caller knows that everything started by the function will be over by the time it's done, if you give it a CoroutineScope receiver, the caller knows the function returns with ongoing background work, that they can cancel later through the scope.
👍 1
j
the conventions say to use suspend for functions that don't create sub tasks, and to use a CoroutineScope receiver for functions that do.
@CLOVIS Your second message is more appropriate I believe. I would be a bit less general here.
suspend
functions can also create subtasks, but all of those subtasks should finish before the function returns. This encourages the use of
coroutineScope { ... }
for launching subtasks that are just meant to parallelize some work of the suspend function. Indeed, the important part about non-suspending extensions on
CoroutineScope
is that they can launch work that outlives their own scope
c
@Joffrey yep, that's why I think in this case CoroutineScope is a good practice.
👍 1
j
I agree, it's good practice if you actually need to encapsulate the launch of some concurrent work in a function. Although in general I would try to use
suspend
functions instead as much as possible, and limit the
CoroutineScope
extensions to a minimum. This specific case is probably a good case for
CoroutineScope
extension, though. I like the idea