기본적인 취소
Job 인터페이스는 취소하게 해주는 cancel 함수를 가지고 있다. 이 함수를 호출하면 다음과 같은 효과가 있다.
- 호출한 코루틴은 첫 번째 중단점(아래 예제 delay)에서 Job 을 끝낸다.
- Job이 자식을 가지고 있다면, 그들 또한 취소가 되지만 부모는 취소되지 않는다.
- Job이 취소되면 취소된 Job은 코루틴의 부모로 사용될 수 없다.
- Cancelling → Cancelled 상태로 변경된다.
fun main() = runBlocking {
coroutineScope {
val job = launch {
repeat(1_000) {
delay(100L)
println("print : $it")
}
}
delay(1000)
job.cancel()
job.join()
println("Success Canceled")
}
}
print : 0
print : 1
print : 2
...
print : 9
Success Canceled
cancel 함수가 호출된 뒤 다음 작업을 진행하기전 취소 과정을 기다리기 위해 join 함수를 호출해야한다.
join 함수를 호출하지 않을 시 경쟁상태 가 될 수 있다.
경쟁 상태란 : 공학 분야에서 경쟁 상태(race condition)란 둘 이상의 입력 또는 조작의 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태를 말한다. 출처 : wiki
이에 코루틴은 cancelAndJoin함수를 제공한다.
public suspend fun Job.cancelAndJoin() {
cancel()
return join()
}
취소는 어떻게 작동하는가 ?
Job이 취소되면 Cancelling 상태로 바뀐다. 상태가 바뀐 뒤 첫 번째 중단점에서 cancellationException을 던진다.
예외는 try-catch구문을 사용해서 잡을 수 있지만 다시 예외를 던지는게 좋다.
fun main() {
runBlocking {
val job = Job()
launch(job) {
runCatching {
repeat(1000) { i ->
delay(200L)
println("job: I'm sleeping $i ...")
}
}.onFailure {
println(it)
}.getOrThrow()
}
delay(1100)
job.cancelAndJoin()
println("Cancelled Successfully")
delay(1000)
}
}
// 결과
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
kotlinx.coroutines.JobCancellationException: Job was cancelled; job=StandAloneCoroutine{Cancelling}@6d311334
Cancelled Successfully
취소 중 코루틴을 한번더 호출하기
Job 은 한번 취소(Cancelling) 상태가 되면 중단하거나 다른 코루틴을 시작하는 것은 불가능하다.
만약 다른 코루틴을 시작하려고 하면 무시하고, 중단하려고 하면 CancellationException을 던진다.
fun main() = runBlocking {
val job = Job()
launch(job) {
runCatching {
delay(2000)
println("Job is Done")
}.onFailure {
println("Finally")
launch { // 한번 취소되었으므로 예외가 발생함
println("Will not be printed")
}
delay(1000)
println("Will not be printed")
}.exceptionOrNull()
}
delay(1000)
job.cancelAndJoin()
println("Job is Cancel")
}
// 결과
Finally
Job is Cancel
코루틴이 이미 취소되었을 때 중단 함수를 반드시 호출해야 하는 상황이 생길 수 있다.
예를들어 데이터베이스를 롤백하는 상황 등 이다.
이럴 경우 withContext(NonCancellable) 을 사용해 Job이 취소될 수 없는 NonCancellable 을 사용하는 것 이다.
withContext : 코드 블럭의 Context을 변경
fun main() = runBlocking {
val job = Job()
launch(job) {
runCatching {
delay(2000)
println("Job is Done")
}.onFailure {
println("Finally")
withContext(NonCancellable) {
delay(1000L)
println("CleanUp Code")
}
delay(1000)
println("Will not be printed")
}.exceptionOrNull()
}
delay(1000)
job.cancelAndJoin()
println("Job is Cancel")
}
// 결과
Finally
CleanUp Code
Job is Cancel
invokeOnCompletion
코루틴 취소를 하는 다른 방법 중 하나는 invokeOnCompletion 함수를 사용하는 것이다.
invokeOnCompletion 의 기능은 Job이 Cancelled, Completed 와 같은 마지막 상태에 도달했을 때 호출될 핸들리러를 지정할 수 있다.
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
public typealias CompletionHandler = (cause: Throwable?) -> Unit
fun main() {
runBlocking {
val job = launch {
delay(1000)
}
job.invokeOnCompletion {
println("Finished")
}
delay(400)
job.cancelAndJoin()
}
}
// result
Finished
CompletionHandler 에 전달되는 cause 는 다음과 같은 의미를 가진다.
- Job 이 예외 없이 완료되면 null 이 된다.
- 코루틴이 취소되었으면 CancellationException이 된다.
중단될 수 없는 걸 중단하기
취소는 중단점에서 일어나기 때문에 중단점이 없으면 취소를 할 수 없다.
이러한 상황을 만들기 위해 delay 함수 대신 Thread.sleep 함수를 사용하겠다.
fun main() = runBlocking {
val job = Job()
launch(job) {
repeat(1_000) {
Thread.sleep(200)
println("Printing $it")
}
}
delay(1000L)
job.cancelAndJoin()
println("Cancelled successful")
delay(1000L)
}
/
Printing 1
Printing 2
Printing 3
...
Printing 999
Cancelled successful
위의 예제 코드에서 우리는 1초에 코드를 중단되기를 의도했는데 실제로는 3분이 넘게 걸린다.
이러한 상황을 대처하기 위해 yield를 호출하는 것이다.
yield 함수는 코루틴을 중단하고 즉시 재실행 하므로, 중단점이 생겼기 때문에 코루틴을 취소와 중단중에 필요한 작업을 할 수 있다.
fun main() = runBlocking {
val job = Job()
launch(job) {
repeat(1_000) {
Thread.sleep(200)
yield()
println("Printing $it")
}
}
delay(1000L)
job.cancelAndJoin()
println("Cancelled successful")
delay(1000L)
}
// result
Printing 0
Printing 1
Printing 2
Printing 3
Printing 4
Cancelled successful
중단할 수 없으면서 CPU나 복잡한 연산들이 중단 함수에 있다면, 각 연산들 사이에 yield를 사용하는 것이 좋다.
또 다른 방법으로는 Job의 Active 상태를 추적해 Activie 하지 않으면 연산을 중단하는 방법 이 있다.
fun main() {
runBlocking {
val job = Job()
launch(job) {
do {
println("I'm sleeping ${Thread.currentThread().name}")
delay(500L)
} while (isActive)
delay(1100)
job.cancelAndJoin()
println("I'm done ${Thread.currentThread().name}")
}
}
}
// result
I'm sleeping main
또 다른 방법으로는 Activie 상태가 아니면 CancellationException을 던지는 ensureActive() 함수를 사용할 수 있다.
suspendCancellableCoroutine
이 함수는 suspendCoroutine과 비슷하지만, CancellableContinuation<T> 로 매핑되어 있다.
이 함수의 기능으로는 코루틴이 취소되었을 때 행동을 invokeOnCancellation을 활용해 구현할 수 있다
'도서 > 코틀린 코루틴' 카테고리의 다른 글
2장 코루틴 라이브러리 - 예외 처리 (1) | 2024.02.03 |
---|---|
2장 코루틴 라이브러리 - Job과 자식 코루틴 기다리기 (0) | 2024.02.03 |
2장 코루틴 라이브러리 - CoroutineContext (1) | 2024.02.03 |
2장 코루틴 라이브러리 - Coroutine Buidler (0) | 2024.02.03 |
코틀린 코루틴 이해하기 + 코루틴 실전 대입하기 (0) | 2024.01.23 |