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?
CodePudding user response:
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.
CodePudding user response:
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")