Reputation: 529
I've been working with Coroutines thinking they work like Java Threads where the isAlive boolean is the same as checking for isInterrupted() in a long running operation such as writing a large file.
In the past the Coroutine has worked as expected but in the current version 1.6.1-native-mt it doesn't seem to work like that any more. In the code below, the code inside the isAlive check doesn't seem to run even when I switch to the 1.6.1 non-multithreaded library.
If this is the way it's supposed to work? What should the correct way of performing a clean-up after a job is cancelled?
Also when attaching a Coroutine to a scope, in this case the Activity, shouldn't it be auto-cancelled when a new Activity is started? It seems to keep running if it isn't cancelled manually like in Java during the onPause()/onStop() phase.
Kotlin Coroutine:
import android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.*
class TestActivity : AppCompatActivity() {
private val tag = "TEST"
lateinit var job: Job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
val button = findViewById<Button>(R.id.stopbutton)
job = lifecycleScope.launch(Dispatchers.IO) {
(1..120).forEach {
// big long operation e.g. writing a large data file
Log.d(tag, "ticking $it")
if ( !isActive ) {
// this code never happens after....
// 1. button is clicked to cancel job
// 2. app is killed
Log.d(tag, "ticking stopped by button")
// clean up the partially written file
return@launch
}
delay(1000)
}
}
button.setOnClickListener {
job.cancel()
}
}
}
Expected Output (Should be same as Java Thread code):
ticking 1
ticking 2
ticking 3
ticking stopped
Actual Coroutine Output:
ticking 1
ticking 2
ticking 3
And the Java code for handling thread interrupts.
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
protected Thread t = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
t = new Thread(){
@Override
public void run() {
for ( int x = 0; x < 120; x++ ) {
Log.d("TEST", "ticking " + x);
if ( interrupted() ) {
Log.d("TEST", "ticking stopped by button");
return;
}
// just for demoing
SystemClock.sleep(1000);
}
}
};
t.start();
Button btn = findViewById(R.id.button);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
t.interrupt();
}
});
}
}
Update 1
After following @Sergio and @Pawel's quick replies I have removed delay() and replaced it with SystemClock.sleep() just like in Java to simulate long operations. Now it works as expected although I wonder why so many tutorials use it without highlighting the issue.
Still trying to figure out what is the point of attaching a coroutine to a lifecycle scope if it doesn't auto cancel() onstop() such as described here:
https://medium.com/androiddevelopers/cancellation-in-coroutines-aa6b90163629
Or maybe I'm missing the point?
Upvotes: 0
Views: 1628
Reputation: 133492
If the coroutine is cancelled, then delay()
or practically any other suspend call may throw CancellationException
. So one approach is to simply use try-catch-finally to clean up on exit, as those are still run.
If you need to call suspend functions during cleanup, you need to use withContext(NonCancellable) { ... }
so that the suspend functions inside that don't immediately throw CancellationException
again.
try {
(1..120).forEach {
// big long operation e.g. writing a large data file
Log.d(tag, "ticking $it")
delay(1000)
}
} catch (ex: CancellationException) {
withContext(NonCancellable) {
Log.d(tag, "ticking cancelled")
// clean up interrupted file generation
}
throw ex
}
Upvotes: 0
Reputation: 17258
Cancellation in coroutines is indeed cooperative and checking isActive
is one way to do it properly.
But you've missed the part that built in suspending functions (in this case delay
) check and throw a CancellationException
internally so you won't get another loop execution after cancellation.
If you want to react to cancellation you can set a CompletionHandler
:
job.invokeOnCompletion {
when(it?.message) {
null -> Log.d("Completed normally")
"ButtonPressed" -> Log.d("Cancelled by button press")
else -> Log.d("Cancelled: ${it.message}")
}
}
And alter your cancellation code so you can discern the cancellation message:
job.cancel("ButtonPressed")
Upvotes: 1
Reputation: 30655
It's because delay()
function internally also checks for cancellations, and if the cancellation happens during its execution it stops a coroutine by throwing a cancellation exception. To get the result you want try to use Thread.sleep()
or similar blocking sleeping methods instead of delay
or try to wrap calling delay()
into try-catch
block.
Upvotes: 1