https://kotlinlang.org logo
#coroutines
Title
# coroutines
e

Exerosis

07/09/2022, 1:42 AM
What is a good way to schedule a repeating task in a platform agnostic way?
Copy code
fun main() = runBlocking {
    val component = Component {
        var a = 0; var b = 0
        every(50.milliseconds) {
            if (it % 20 == 0) println("A: ${a++}")
        }
        every(1.seconds) {
            println("B: ${b++}")
        }
    }
    component.enable()
    delay(50.seconds)
    println("Done!")
}
context(Toggled) @Base
suspend fun every(period: Duration, block: suspend (Int) -> (Unit)) {
    simultaneously {
        var i = 0
        while (isActive) {
            block(i++)
            delay(period)
        }
    }
}
The problem with this is that over time the two tasks get way out of sync: A: 47 B: 49 After 50 seconds
f

Fleshgrinder

07/09/2022, 6:32 AM
This is normal, other things are running, clocks are not reliable, ... scheduling things is always best effort.
a

Alejandro Serrano Mena

07/09/2022, 7:23 AM
e

Exerosis

07/09/2022, 8:25 AM
I agree scheduling is normally best effort, but like ScheduledExecutor will correct itself rather than getting further and further off schedule IIRC. I'll checkout Arrow's solution that looks great thank you very much!
u

uli

07/09/2022, 1:02 PM
You can get an initial time stamp and then delay for
startTime + i * period - now()
☝️ 1
Copy code
var nextTick : Long = now()
while(isActive()) {
  block(nextTick)
  nextTick += period
  delay(nextTick - now())
}
If you really need
i
add as required.
This will keep your error constant, bounded by the capabilities of the scheduler/expected thread congestion
s

Steven Veltema

07/11/2022, 6:10 AM
This approach will still drift over time but it might not matter if your period is long enough and your task is relatively short. If you need more precision, a
fixedRateTimer
(or
NSTimer
on iOS) is the only way to get tight non-drifty periodicity.
u

uli

07/11/2022, 6:28 AM
Why would it drift, assuming you have a decent clock? My theorie goes each error is corrected in the next interval so errors do not accumulate.
s

Steven Veltema

07/11/2022, 6:33 AM
I suggest closely testing your interval loop. I was trying the same approach for a fairly precise 500ms ticker on mobile and it would drift by seconds over the course of 10 minutes. The milliseconds add up if your interval is short enough.
e

Exerosis

07/11/2022, 5:41 PM
What does the fixedRateTimer do that the nextTick concept does not?
s

Steven Veltema

07/12/2022, 11:55 AM
It’s implemented with
java.util.Timer
underneath which doesn’t have a real-time guarantee but in my testing it offers more consistent timing than using
delay
. Also
fixedRateTimer
has the added bonus of being part of the jvm stdlib.
u

uli

07/12/2022, 11:58 AM
The timing qualities of
delay
probably depend on the thread you schedule on. If you delay on a busy dispatcher you are off. But non of that should cause a drift (i.e. accumulate errors).
e

Exerosis

07/13/2022, 4:42 AM
Right now I'm seeing pretty good consistency using nextTick, no drift. Obviously, on a busy or single-threaded dispatcher, the average time between calls isn't perfect but that's ok for me.
👍 2
d

DALDEI

07/27/2022, 11:05 PM
I have done the same thing, and measured the drift and jitter between a delay() based simple scheduler (same concept, each delay use the (next interval - current time) -- I measured best using delay over Thread.sleep() or java.util.Timer over long periods. If drift is accumulating -- it must be due to some bug -- One factor - think through the case where the work takes longer then a cycle, that can accumulate drift BIG TIME if you dont accommodate it or have a bug in it due to getting delay going negative. When I accounted for the following -- I got a very stable cycle +/- 1ms usually (when measured with NS timer) val CYCLE=1000 var start = now() var tick = 0 while(true){ doWork() val sleepUntil = start + tick++ * CYCLE delay( sleepUntil - now ) } if doWork() takes longer then 1000ms ever then you get out of step and delay for a negative value (or zero if you ensureAtLeast) once that happens then you can start drifting if using something like above -- unless you decide what to do> In my case I would do the equivilent of bumping up tick until it reached the present This has the effect of skipping a cycle . In cases like this you have to tollerate either conserving the number of ticks or conserving the periodicity, you cant do both
658 Views