Vasiliy
Vasiliy

Reputation: 16268

Kotlin Coroutines: what's the diffrence between using NonCancellable and a standalone new Job

In Coroutines, when I want to guard a block of code against cancellation, I should add NonCancellable to the Context:

@Test
fun coroutineCancellation_NonCancellable() {
    runBlocking {
        val scopeJob = Job()
        val scope = CoroutineScope(scopeJob + Dispatchers.Default + CoroutineName("outer scope"))
        val launchJob = scope.launch(CoroutineName("cancelled coroutine")) {
            launch (CoroutineName("nested coroutine")) {
                withContext(NonCancellable) {
                    delay(1000)
                }
            }
        }
        scope.launch {
            delay(100)
            launchJob.cancel()
        }
        launchJob.join()
    }
}

The above unit test will take ~1.1sec to execute, even though the long-running Coroutine is cancelled after just 100ms. That's the effect of NonCancellable and I understand this point.

However, the below code seems to be functionally equivalent:

@Test
fun coroutineCancellation_newJobInsteadOfNonCancellable() {
    runBlocking {
        val scopeJob = Job()
        val scope = CoroutineScope(scopeJob + Dispatchers.Default + CoroutineName("outer scope"))
        val launchJob = scope.launch(CoroutineName("cancelled coroutine")) {
            launch (CoroutineName("nested coroutine")) {
                withContext(Job()) {
                    delay(1000)
                }
            }
        }
        scope.launch {
            delay(100)
            launchJob.cancel()
        }
        launchJob.join()
    }
}

I tried to find any functional differences between these two approaches in terms of cancellation, error handling and general functionality, but so far I found none. Currently, it looks like NonCancellable is in the framework just for readability.

Now, readability is important, so I'd prefer to use NonCancellable in code. However, its documentation makes it sound like it is, in fact, somehow different from a regular Job, so I want to understand this aspect in details.

So, my quesiton is: is there any functional difference between these two approaches (i.e. how can I modify these unit tests to have difference in outcomes)?

Edit:

Following Louis's answer I tested "making cleanup non-cancellable" scenario and in this case Job() also works analogous to NonCancellable. In the below example, unit test will run for more than 1sec, even though the coroutine is cancelled just after 200ms:

@Test
fun coroutineCancellation_jobInsteadOfNonCancellableInCleanup() {
    runBlocking {
        val scope = CoroutineScope(Job() + Dispatchers.Default + CoroutineName("outer scope"))
        val launchJob = scope.launch(CoroutineName("test coroutine")) {
            try {
                delay(100)
                throw java.lang.RuntimeException()
            } catch (e: Exception) {
                withContext(Job()) {
                    cleanup()
                }
            }
        }
        scope.launch {
            delay(200)
            launchJob.cancel()
        }
        launchJob.join()
    }
}

private suspend fun cleanup() {
    delay(1000)
}

Upvotes: 3

Views: 4002

Answers (1)

Louis CAD
Louis CAD

Reputation: 11527

NonCancellable doesn't respond to cancellation, while Job() does.

NonCancellable implements Job in a custom way, and it doesn't have the same behavior as Job() that is using cancellable implementation.

cancel() on NonCancellable is no-op, unlike for Job() where it would cancel any child coroutine, and where any crash in the child coroutines would propagate to that parent Job.

Upvotes: 5

Related Questions