-
[Coroutine] 코루틴 학습 - 7 (Cancellation)Java/Kotlin 2022. 5. 3. 10:12반응형
Cancellation
- Job 인터페이스는 cancel() 메서드를 가지고 있다.
- 코루틴은 첫 번째 일시중단 지점에서 job을 중단한다.
- job에 자식 코루틴이 포함되어 있다면 자식 코루틴 또한 cancel 된다.
- job이 cancel되고 나면, 부모 코루틴으로써 새로운 코루틴 실행할 수 없다.
suspend fun main(): Unit = coroutineScope { val job = launch { repeat(1_000) { i -> delay(200) println("Printing number $i") } } delay(1100) job.cancel() job.join() println("Cancelled successfully") } // Printing number 0 // Printing number 1 // Printing number 2 // Printing number 3 // Printing number 4 // Cancelled successfully
- cancel 과 exception의 차이점은 cause를 명시하는 방법이다.
- 코루틴의 cancel에서 cause는 CancellationException의 서브타입만 사용할 수 있다.
- cancel 후에는 join을 함께 사용하여 cancellation이 완료될 때까지 기다려야 한다. 그렇지 않으면 레이스 컨디션이 발생할 수 있다.
suspend fun main() = coroutineScope { val job = launch { repeat(1_000) { i -> delay(100) Thread.sleep(100) println("Printing number $i") } } delay(1000) job.cancel() println("Cancelled successfully") } // Printing number 0 // Printing number 1 // Printing number 2 // Printing number 3 // Cancelled successfully // Printing number 4
위 코드에서는 cancel을 사용하면서 join()을 호출하지 않았는데, "Cancelled successfully" 뒤에 "Printing number 4"가 출력된 것을 확인할 수 있다. job.join()은 cancellation이 완료될 때까지 일시중단 시키기 때문에 이러한 차이가 발생하는 것이다.
join()과 cancel()을 매번 함께 써주기에는 매우 성가시고, 실수할 여지도 있다. 그래서 좀더 편하게 코드를 작성할 수 있도록 cancelAndJoin() 함수가 제공된다. cancelAndJoin() 함수는 kotlinx.coroutines 라이브러리에서 편의를 위해 제공하는 확장 함수이다.
public suspend fun Job.cancelAndJoin() { cancel() return join() }
Cancellation의 동작
- job이 cancel 되었을 때, job의 상태는 'cancelling'이 된다.
- 그리고 첫 번째 일시중단 지점에 CancellatoinException 익셉션이 던져진다.
- CancellationException은 try-catch로 잡을 수 있지만 필요한 작업을 수행한 후, 다시 throw 하는 것이 좋다.
suspend fun main(): Unit = coroutineScope { val job = Job() launch(job) { try { repeat(1_000) { i -> delay(200) println("Printing number $i") } } catch (e: CancellationException) { println(e) throw e } } delay(1100) job.cancelAndJoin() println("Cancelled successfully") delay(1000) } // Printing number 0 // Printing number 1 // Printing number 2 // Printing number 3 // Printing number 4 // JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@2c3e373 // Cancelled successfully
- canceled된 코루틴은 단순히 중지되는 것이 아니라 내부적으로 예외를 사용하여 canceled 된다.
- finally block을 사용해서 자원을 닫는 메커니즘을 이용할 수도 있다. (데이터베이스 커넥션이나 file 등..)
suspend fun main(): Unit = coroutineScope { val job = Job() launch(job) { try { delay(200) println("Job is done") } finally { println("Finally") launch { // 이 코루틴은 무시된다 println("Will not be printed") } delay(100) // 예외가 발생한다 println("Will not be printed") } } delay(100) job.cancelAndJoin() println("Cancel done") } // Finally // Cancel done
- 코루틴은 catch문을 통해서 CancellationException에 대해서 예외처리를 할 수 있지만, 일시중단은 더이상 허용되지 않는다.
- Job이 Cancelling 상태일 때, 일시중단이나 다른 코루틴이 시작되는 것을 허용하지 않는다.
- 새로운 코루틴을 시작하려 한다면 해당 코루틴은 무시된다.
- 일시중단을 시작하려 한다면 CancellationException을 던지고, finally block이 호출된다.
- 만약 cancel 되더라도 일시중단 함수를 사용해야 한다면, withContext(NonCancellable) 함수를 사용하면 된다.
- withContext는 코드 block의 컨텍스트를 바꾼다.
- NanCancellable 객체는 cancel 할 수 없는 Job이다.
- block 내부에서 job은 'Active'상태이며, 언제든 일시중단 함수를 호출할 수 있다.
suspend fun main(): Unit = coroutineScope { val job = Job() launch(job) { try { delay(200) println("Coroutine finished") } finally { println("Finally") withContext(NonCancellable) { delay(1000L) println("Cleanup done") } } } delay(100) job.cancelAndJoin() println("Done") } // Finally // Cleanup done // Done
invokeOnCompletion
- 리소스를 해제하기 위해 종종 사용하는 Job의 다른 메커니즘으로는 invokeOnCompletion() 함수가 있다.
- invokeOnCompletion() 함수는 핸들러를 지정하여 job이 종료 상태('Completed' 또는 'Cancelled')가 될 때 호출되도록 한다.
suspend fun main(): Unit = coroutineScope { val job = launch { delay(1000) } job.invokeOnCompletion { exception: Throwable? -> println(exception) println("Finished") } delay(400) job.cancelAndJoin() } // kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@ea99795 // Finished
- 핸들러의 exception 파라미터는 job이 Exception 발생없이 완료되면 null로 받는다.
- 코루틴이 취소되면 CancellationException 타입으로 받는다.
코루틴이 멈추지 않을 때..
- cancellation은 일시중단 지점에서 일어나기 때문에 일시중단 지점이 없으면 취소되지 않는다.
- 아래의 코드에서는 delay 대신에 Thread.sleep()을 호출하고 있는데, 이는 예시를 위해 작성된 코드일 뿐 실제로는 사용해서는 안된다.
suspend fun main(): Unit = coroutineScope { val job = Job() launch(job) { repeat(1_000) { i -> Thread.sleep(200) println("Printing $i") } } delay(1100) job.cancelAndJoin() println("Cancelled successfully") delay(1000) } // Printing 0 // Printing 1 // Printing 2 // Printing 3 // ... // Printing 999
- 위 코드는 일시중단 지점이 없기 때문에 코루틴이 취소되지 않는다.
- 이러한 경우에 yield() 함수를 매 반복마다 사용해주면 취소할 수 있다.
- yield()는 일시중단하고 코루틴을 즉시 재개시킨다.
- yield()는 cancellation(또는 dispatcher를 이용한 쓰레드 변경)을 포함한 일시중단 중에 필요한 공간을 제공한다.
suspend fun main(): Unit = coroutineScope { val job = Job() launch(job) { repeat(1_000) { i -> Thread.sleep(200) yield() println("Printing $i") } } delay(1100) job.cancelAndJoin() println("Cancelled successfully") delay(1000) } // Printing 0 // Printing 1 // Printing 2 // Printing 3 // Printing 4 // Cancelled successfully
- 또 다른 방법으로는 job의 상태를 추적하는 방법이 있다.
- coroutine builder는 해당 builder의 scope를 참조하고 있다. CoroutineScope는 context를 가지고 있고, coroutineContext 프로퍼티를 이용하여 접근할 수 있다.
- coroutineContext[Job]을 통해서 job에 접근할 수 있고, 아래와 같이 job의 현재 상태가 무엇인지 체크할 수 있다.
suspend fun main(): Unit = coroutineScope { val job = Job() launch(job) { do { Thread.sleep(200) println("job is active - $isActive") } while (isActive) } delay(1100) job.cancelAndJoin() println("Cancelled!") delay(1000) } // job is active - true // job is active - true // job is active - true // job is active - true // job is active - true // job is active - false // Cancelled!
- do-while문을 사용하는 대신 ensureActive() 함수를 사용할 수도 있다.
- ensureActive() 함수는 Job이 'Active' 상태가 아니면 CancellationException을 던진다.
suspend fun main(): Unit = coroutineScope { val job = Job() launch(job) { repeat(1_000) { i -> Thread.sleep(200) ensureActive() println("Printing number $i") } } delay(1100) job.cancelAndJoin() println("Cancelled!") delay(1000) } // Printing number 0 // Printing number 1 // Printing number 2 // Printing number 3 // Printing number 4 // Cancelled!
ensureActive()와 yield()의 차이
위의 예시로만 보면 두 함수가 유사해 보이지만 아래와 같은 차이점이 있다.
- ensureActive()
- CoroutineScope(또는 CoroutineContext, Job)에서 호출된다.
- job이 더이상 'Active' 상태가 아니라면 예외를 던진다.
- 조금 더 일반적으로 사용되고, 더 가볍다.
- yield()
- 일반적인 최상위 일시중단 함수다.
- scope를 필요로 하지 않으므로 일반적인 일시중단 함수에서 사용될 수 있다.
- 일시 중단 및 재개를 수행하기 때문에 쓰레드 풀과 함께 디스패처를 사용하면 쓰레드가 변경되는 등 다른 영향이 발생할 수 있습니다.
- 조금 더 CPU 집약적인 작업이나 쓰레드를 블로킹하는 일시중단 함수에서 자주 사용된다.
suspendCancellableCoroutine
- suspendCancellableCoroutine() 함수는 suspendCoroutine처럼 동작하지만, continuatnion을 추가적인 메서드를 제공하는 CancellableContinuation<T>으로 래핑하고 있다.
- CancellableContinuation<T>이 제공하는 중요한 메서드 중 하나로는 invokeOnCancellation()이 있다. 이는 코루틴이 취소되었을 때 처리해야할 작업을 지정할 수 있다.
fun main(): Unit = runBlocking { val hello = createHelloFoo("I'm not hello") println(hello) } suspend fun createHelloFoo(name: String): Foo = suspendCancellableCoroutine { continuation -> continuation.invokeOnCancellation { println("Cancellation invoked!!") } if (name != "hello") { continuation.cancel(Exception("name is not hello")) } else { continuation.resume(Foo("hello")) } } data class Foo( val name: String, )
참고자료
https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837
반응형'Java > Kotlin' 카테고리의 다른 글
[Coroutine] 코루틴 학습 - 9 (Coroutine scope function) (0) 2022.05.07 [Coroutine] 코루틴 학습 - 8 (Exception handling) (0) 2022.05.05 [Coroutine] 코루틴 학습 - 6 (Job and children awaiting) (0) 2022.05.01 [Coroutine] 코루틴 학습 - 5 (Coroutine context) (0) 2022.04.28 [Coroutine] 코루틴 학습 - 4 (Structured Concurrency) (0) 2022.04.27