Pat Teruel
03/10/2023, 9:28 PMGlobalScope.async
? Or is there an alternative way to do a DispatchQueue.global().async
on KMM?
My problem is, KMM doesn’t have a close-to-native way of Scheduling tasks without freezing the Suspend function. So I had to create a class that’s “invalidatable”, similar to that of the Timer in Swift.
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
class AsyncSchedulerCaller(
delayTime: Long,
suspendFunction: suspend () -> Unit
) {
init {
GlobalScope.async {
delay(delayTime)
if (invalidated) {
println("Invalidated.")
} else {
isRunning = true
suspendFunction()
isRunning = false
}
}
}
var invalidated = false
var isRunning = false
fun invalidate() {
invalidated = true
}
}
The way I use this is I store it in an optional variable and invalidate if it exists and create a new one, e.g.
class SomeClass {
var scheduler: AsyncSchedulerCaller? = null
suspend fun someSuspendFunction() {
runSomeApiCallHere()
scheduler?.invalidate() // this will make sure that the existing scheduled task will not be run
scheduler = AsyncSchedulerCaller(
delayTime = 500, // will run after 500 milliseconds
suspendFunction = {
runSomeOtherApiCallHere()
}
)
}
}
To be honest, it already runs properly in iOS, and the scheduled task seem to run in the background. But I want to understand why it does, else, I might encounter bugs and I wouldn’t know how to fix.
That said, it works in iOS, but I haven’t tested in Android. Would this kind of code be dangerous on the app’s lifecycle in Android?
Thanks for the answers.Jeff Lockhart
03/11/2023, 7:01 PMGlobalScope.async
(although in this case I'd recommend simply GlobalScope.launch
, as you're not needing to await an async returned value) is pretty similar to iOS's DispatchQueue.global().async
, except it uses Kotlin's coroutines instead of GCD queues. It's only "dangerous" in that it doesn't take advantage of Kotlin coroutine's structured concurrency. `GlobalScope`'s lifecycle is the life of your app. So the coroutine's async work isn't automatically cancelled at the end of a more intentionally defined `CoroutineScope`'s lifecycle.
Your code does work, but the coroutine lives longer than it needs to before determining it's no longer needed. You could do something like this instead, and explicitly cancel the running coroutine when it's invalidated:
class AsyncSchedulerCaller(
delayTime: Long,
suspendFunction: suspend () -> Unit
) {
private val job = GlobalScope.launch {
delay(delayTime)
isRunning = true
suspendFunction()
isRunning = false
}
var isRunning = false
private set
fun invalidate() {
job.cancel()
println("Invalidated.")
}
}
If invalidate()
is being called from iOS, this may be the most straightforward solution. Alternatively, if you're able to define a `CoroutineScope` that is `cancel()`ed when you'd be invalidating, you could pass the scope and have the coroutine cancelled for you:
class AsyncSchedulerCaller(
delayTime: Long,
scope: CoroutineScope,
suspendFunction: suspend () -> Unit
) {
private val job = scope.launch {
try {
delay(delayTime)
} catch (e: CancellationException) {
println("Invalidated.")
throw e
}
isRunning = true
suspendFunction()
isRunning = false
}
var isRunning = false
private set
}
The advantage of using CoroutineScope
is that you can run multiple coroutines within the scope and they will all be cancelled when the scope is cancelled at the end of its lifecycle.Big Chungus
03/11/2023, 10:21 PMPat Teruel
03/13/2023, 1:35 AMasync
value into a variable and realize it's a Job type, so yeah I pretty much added the same code you did here.
Another question though. Since I don't want the coroutine to live longer than it should, can I actually just "invalidate", or cancel the job INSIDE the coroutine scope after the suspend function is executed completely? or is it a bad practice to do that?Jeff Lockhart
03/13/2023, 5:10 AMsuspendFunction()
completes, if I understand your question correctly.
There's nothing wrong with cancelling a coroutine job from within its suspend function. But if you do need to cancel a coroutine early from within its own suspend function, it's generally easier to just return early. Cancelling a coroutine's job only works to cancel a coroutine while it's in a cancellable state, where the code is actively checking the cancelled state of the coroutine and returning early or throwing CancellationException
in response. Well behaving long-running suspend functions, such as delay()
, will do this for you. You can also simply throw CancellationException
at any point in a running coroutine to cancel it. See docs on coroutine cancellation.
Martynas brings up a good question. You mention needing to freeze your suspend function. Are you not using the new Kotlin/Native memory model?Pat Teruel
03/14/2023, 1:54 AMsuspend fun a() {
b()
}
suspend fun b() {
delay(1000)
}
When I do it this way, a()
will not be finished until b()
is finished.
What I wanted to do, as I already mentioned on this thread, is to not have a()
wait for b()
to finish, and instead schedule b()
to be executed IF there’s no “new” b()
to be executed.
so:
suspend fun a() {
existingCaller?.invalidate()
existingCaller = AsyncCaller {
b()
}
}
Maybe the terms I used is wrong. But anyway, I’m using the new memory model because it’s awesome. That’s the one in the gradle properties, right?
#Native
kotlin.native.binary.objcExportSuspendFunctionLaunchThreadRestriction=none
Jeff Lockhart
03/14/2023, 6:00 AMkotlin.native.binary.memoryModel=experimental
But it's enabled by default since Kotlin 1.7.20.
The property
kotlin.native.binary.objcExportSuspendFunctionLaunchThreadRestriction=none
is an additional option that can be enabled with the new memory model though.
The term "freezing" was just a bit confusing because the old memory model required freezing objects as read-only in order to access an object from multiple threads.
I understand your intention is just to be able to replace a previously scheduled suspend routine with a new routine. 👍🏼