Java/Kotlin
[Coroutine] 코루틴 학습 - 7 (Cancellation)
Icarus8050
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
반응형