ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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

    반응형

    댓글

Designed by Tistory.