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 PMCoroutineScope
bezrukov
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