Java's ScheduledExecutorService
has a nifty scheduleAtFixedRate
API, which allows you to schedule a Runnable
to be executed in a recurring manner.
Mimicking scheduleAtFixedRate
seems easy, after all, isn't it just a while (true)
loop that invokes your task every once in a while?
GlobalScope.launch {
while (true) {
println("Hello World!! Loritta is so cute :3")
delay(5_000)
}
}
And it seems to work fine, it does print Hello World!! Loritta is so cute :3
every 5s! And you may be wondering: What's wrong with it?
What you implemented is a scheduleAtFixedDelay
-like API!
Let's change your code a bit, so we can see the problem in your implementation.
GlobalScope.launch {
while (true) {
println("Hello World!! Loritta is so cute :3")
Thread.sleep(5_000) // This is a blocking call
delay(5_000)
}
}
The output will be:
00:00: Hello World!! Loritta is so cute :3
00:10: Hello World!! Loritta is so cute :3
00:20: Hello World!! Loritta is so cute :3
This is not how scheduleAtFixedRate
works, this is how scheduleAtFixedDelay
works! Rate
schedules the next execution based on the start of the current task, while Delay
schedules the next execution based on the end of the current task. You can learn more about this in this StackOverflow answer that I always come back when I need to refresh my memory about what's the difference between the two.
A quick and dirty solution is launch
ing a new coroutine instead of blocking the current coroutine.
GlobalScope.launch {
while (true) {
GlobalScope.launch {
println("Hello World!! Loritta is so cute :3")
Thread.sleep(5_000) // This is a blocking call
}
delay(5_000)
}
}
The output will be:
00:00: Hello World!! Loritta is so cute :3
00:05: Hello World!! Loritta is so cute :3
00:10: Hello World!! Loritta is so cute :3
It fixed our issue, yay!! However, let's change your code once again.
GlobalScope.launch {
while (true) {
GlobalScope.launch {
println("Hello World!! Loritta is so cute :3")
Thread.sleep(15_000) // This is a blocking call
}
delay(5_000)
}
}
The output will be:
00:00: Hello World!! Loritta is so cute :3
00:05: Hello World!! Loritta is so cute :3
00:10: Hello World!! Loritta is so cute :3
And if we look at scheduleAtFixedRate
's docs...
If any execution of this task takes longer than its period, then subsequent executions may start late, but will not concurrently execute.
But it is being executed concurrently! So this doesn't work like scheduleAtFixedRate
's, but it sure does get close to it!
However, we are getting closer! Let's think... "but will not concurrently execute"... What can block concurrent executions...?
Wait, I know! Mutex!
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.time.Duration
/**
* Schedules [action] to be executed on [scope] every [period] with a [initialDelay]
*/
fun scheduleCoroutineAtFixedRate(scope: CoroutineScope, period: Duration, initialDelay: Duration = Duration.ZERO, action: RunnableCoroutine) {
scope.launch {
delay(initialDelay)
val mutex = Mutex()
while (true) {
launch {
mutex.withLock {
action.run()
}
}
delay(period)
}
}
}
fun interface RunnableCoroutine {
suspend fun run()
}
Because the RunnableCoroutine
is a functional interface, you can call it by passing a block to it.
val scope = CoroutineScope(Dispatchers.Default)
scheduleCoroutineAtFixedRate(scope, 5.seconds) {
println("Hello World!! Loritta is so cute :3")
}
Or you can pass an instance of a class/object.
val scope = CoroutineScope(Dispatchers.Default)
scheduleCoroutineAtFixedRate(scope, 5.seconds, PantufaIsCuteRunnable())
class PantufaIsCuteRunnable : RunnableCoroutine {
override suspend fun run() {
println("Hello World!! Pantufa is so cute :3")
}
}
Have fun!