Scott Christopher
09/23/2021, 10:31 AMlaunch 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?Scott Christopher
09/23/2021, 10:33 AMfun CoroutineScope.runThing() {
launch {
delay(1000)
println("hello")
}
}
then somewhere else
coroutineScope {
runThing()
// <snip>
}CLOVIS
09/23/2021, 12:01 PMCLOVIS
09/23/2021, 12:06 PMcoroutineScope.
EtcScott Christopher
09/23/2021, 12:08 PMbezrukov
09/23/2021, 12:15 PMTo explain why: this lets the caller decide how they should deal with cancellationSame 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:
newScope.launch { runThing() }
If they want to wait for the results, they can usethey just have to call.coroutineScope
runThing
So my point is that if it's still possible to control the flow of the runThing fun doesn't make it good practiceScott Christopher
09/23/2021, 12:17 PMJob that is being launched. So it still allows for that case.Scott Christopher
09/23/2021, 12:20 PMdata 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)
}Scott Christopher
09/23/2021, 12:26 PMlaunch from the above code and place that responsibility on the call site?bezrukov
09/23/2021, 12:27 PMScott Christopher
09/23/2021, 12:30 PMCoroutineScopebezrukov
09/23/2021, 12:39 PMCoroutineScope(currentCoroutineContext()).launch {
}
but I don't think it's good practice tooCLOVIS
09/23/2021, 2:11 PMCLOVIS
09/23/2021, 2:13 PMsuspend, 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.Joffrey
09/23/2021, 2:17 PMthe 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 scopeCLOVIS
09/23/2021, 2:18 PMJoffrey
09/23/2021, 2:21 PMsuspend 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