I am trying to run a recursive function on a background thread and be able to cancel it and restart it when the user clicks a button. I have read through some coroutine basics but still not getting the hang of it. Can anyone suggest the best solution?
Also, I'm trying to choose between two versions of the recursive function. One returns its value up through the stack, while the other just updates a class property without returning. Both seem to work properly with the second one finishing slightly faster. Are there any inherent issues with running a recursive function without returns?
Here is my code. It compiles and runs properly in the android emulator (of course I need to click "Wait" on the ANR popup).
The sample data finishes in about 95 or 91 seconds. In reality the list could contain any number of Items with each having a list of any number of integers.
The intention is for the user to submit a set of data for the purpose of calculating probabilies. The function (at this point) counts the total possible combinations. The user should not need to wait for a calculation to complete before clicking the button to submit a new set of data which should cancel the currently running job (if one exists) and start over again with the new data set.
data class Item(var numbers: List<Int>)
class CalcEngine(
) {
val data: List<Item> = listOf(
Item( listOf(1,2,3,4,5,6,7,8,9,10,11,12) ),
Item( listOf(1,2,3,4,5,6,7,8,9,10,11,12) ),
Item( listOf(1,2,3,4,5,6,7,8,9,10,11,12) ),
Item( listOf(1,2,3,4,5,6,7,8,9,10,11,12) ),
Item( listOf(1,2,3,4,5,6,7,8,9,10,11,12) ),
Item( listOf(1,2,3,4,5,6,7,8,9,10,11,12) ),
Item( listOf(1,2,3,4,5,6,7,8,9,10,11,12) ),
Item( listOf(1,2,3,4,5,6,7,8,9,10,11,12) ),
Item( listOf(1,2,3,4,5,6,7,8,9,10,11,12) ),
Item( listOf(1,2,3,4,5,6,7,8,9,10,11,12) ),
)
fun calculate() {
val time1 = measureTimeMillis {
Log.d(TAG, "starting countWithReturn()...")
Log.d(TAG, "count: ${countWithReturn()}")
Log.d(TAG, "done.")
}
Log.d(TAG, "Completed in ${time1 / 1000L }s.")
// sample data finishes in 95s.
Log.d(TAG, "")
testCount = 0UL
val time2 = measureTimeMillis {
Log.d(TAG, "starting countNoReturn()...")
countNoReturn()
Log.d(TAG, "count: $testCount")
Log.d(TAG, "done.")
}
Log.d(TAG, "Completed in ${time2 / 1000L }s.")
// sample data finishes in 91s.
Log.d(TAG, "===================================")
Log.d(TAG, "")
}
fun countWithReturn(index: Int = 0): ULong {
if(index == data.size - 1 ) {
var count = 0UL
for(i in 0..<data[index].numbers.size) { count++ }
return count
} else {
var count = 0UL
for (i in 0..<data[index].numbers.size) {
count = count + countWithReturn(index + 1)
}
return count
}
}
var testCount: ULong = 0UL
fun countNoReturn(index: Int = 0) {
if(index == data.size - 1 ) {
for(i in 0..<data[index].numbers.size) { testCount++ }
} else {
for (i in 0..<data[index].numbers.size) { countNoReturn(index + 1) }
}
}
}
You should use coroutines to control the execution of the calculation. When you make calculate
(and countWithReturn
and countNoReturn
) a suspend function their execution can be cancelled. Since coroutines in Kotlin are cooperative, the part of the code that want's to honor a cancellation request must actively check if the current coroutine is still active by calling
coroutineContext.ensureActive()
This provides a breaking point where your function can be aborted and should be interspersed in your code wherever you want to allow the calculation to be stopped. In your example code a fitting place would be right at the start of countWithReturn
and countNoReturn
.
When you then call calculate
from a coroutine you can cancel that at any time and start it anew. It could look like this:
class SomeContainer(val scope: CoroutineScope) {
var job: Job? = null
fun runCalculation() {
job?.cancel()
job = scope.launch {
CalcEngine().calculate()
}
}
}
Whenever runCalculation
is called the already running coroutine from the previous call (when it exists) is cancelled and a new coroutine is launched. This container class can be anything, f.e. your view model.
Just make sure to always create a new CalcEngine
instance when you start a new calculation because you have a mutable instance variable there (testCount
) which isn't thread-safe.