简体   繁体   中英

Coroutine Lifecycle Scope and isActive not working?

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?

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.

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")

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
  }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM