취소와 타임아웃
예제 14: Job에 대해 취소
명시적인 Job에 대해 cancel 메서드를 호출해 취소할 수 있습니다.
import kotlinx.coroutines.*
suspend fun doOneTwoThree() = coroutineScope {
    val job1 = launch {
        println("launch1: ${Thread.currentThread().name}")
        delay(1000L)
        println("3!")
    }
    val job2 = launch {
        println("launch2: ${Thread.currentThread().name}")
        println("1!")
    }
    val job3 = launch {
        println("launch3: ${Thread.currentThread().name}")
        delay(500L)
        println("2!")  
    }
    delay(800L)
    job1.cancel()
    job2.cancel()
    job3.cancel()
    println("4!")
}
fun main() = runBlocking {
    doOneTwoThree()
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}
delay 간격들을 변경해 보면서 테스트 해보세요.
예제 15: 취소 불가능한 Job
아래의 예제는 취소가 불가능한 Job입니다.
launch(Dispatchers.Default)는 그 다음 코드 블록을 다른 스레드에서 수행을 시킬 것입니다. 나중에 자세히 알아볼테니 지금은 넘어갑시다.
import kotlinx.coroutines.*
suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L
        while (i <= 10) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
    
    delay(200L)
    job1.cancel()
    println("doCount Done!")
}
fun main() = runBlocking {
    doCount()
}
두가지 부분이 신경이 쓰입니다.
  - job1이 취소든 종료든 다 끝난 이후에- doCount Done!을 출력하고 싶다.
- 취소가 되지 않았다.
먼저 취소든 종료든 다 끝난 이후에 doCount Done!을 출력합시다.
예제 16: cancel과 join
import kotlinx.coroutines.*
suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L
        while (i <= 10) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
    
    delay(200L)
    job1.cancel()
    job1.join()
    println("doCount Done!")
}
fun main() = runBlocking {
    doCount()
}
cancel 이후에 join을 넣어서 실제로 doCount가 끝날 때 doCount Done!가 출력하게 했습니다.
예제 17: cancelAndJoin
cancel을 하고 join을 하는 일은 자주 일어나는 일이기 때문에 한번에 하는 cancelAndJoin이 준비되어 있습니다.
import kotlinx.coroutines.*
suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L
        while (i <= 10) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
    
    delay(200L)
    job1.cancelAndJoin()
    println("doCount Done!")
}
fun main() = runBlocking {
    doCount()
}
예제 18: cancel 가능한 코루틴
isActive를 호출하면 해당 코루틴이 여전히 활성화된지 확인할 수 있습니다. isActive를 루프에 추가해봅시다.
import kotlinx.coroutines.*
suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L
        while (i <= 10 && isActive) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
    
    delay(200L)
    job1.cancelAndJoin()
    println("doCount Done!")
}
fun main() = runBlocking {
    doCount()
}
예제 19: finally를 같이 사용
launch에서 자원을 할당한 경우에는 어떻게 정리해야할까요?
suspend 함수들은 JobCancellationException를 발생하기 때문에 표준 try catch finally로 대응할 수 있습니다.
import kotlinx.coroutines.*
suspend fun doOneTwoThree() = coroutineScope {
    val job1 = launch {
        try {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("3!")
        } finally {
            println("job1 is finishing!")
        }
    }
    val job2 = launch {
        try {
            println("launch2: ${Thread.currentThread().name}")
            delay(1000L)
            println("1!")
        } finally {
            println("job2 is finishing!")
        }
    }
    val job3 = launch {
        try {
            println("launch3: ${Thread.currentThread().name}")
            delay(1000L)
            println("2!")
        } finally {
            println("job3 is finishing!")
        }
    }
    delay(800L)
    job1.cancel()
    job2.cancel()
    job3.cancel()
    println("4!")
}
fun main() = runBlocking {
    doOneTwoThree()
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}
예제 20: 취소 불가능한 블록
어떤 코드는 취소가 불가능해야 합니다. withContext(NonCancellable)을 이용하면 취소 불가능한 블록을 만들 수 있습니다.
import kotlinx.coroutines.*
suspend fun doOneTwoThree() = coroutineScope {
    val job1 = launch {
        withContext(NonCancellable) {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("3!")
        }
        delay(1000L)
        print("job1: end")
    }
    val job2 = launch {
        withContext(NonCancellable) {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("1!")
        }
        delay(1000L)
        print("job2: end")
    }
    val job3 = launch {
        withContext(NonCancellable) {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("2!")
        }
        delay(1000L)
        print("job3: end")
    }
    delay(800L)
    job1.cancel()
    job2.cancel()
    job3.cancel()
    println("4!")
}
fun main() = runBlocking {
    doOneTwoThree()
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}
취소 불가능한 코드를 finally절에 사용할 수도 있습니다.
예제 21: 타임 아웃
일정 시간이 끝난 후에 종료하고 싶다면 withTimeout을 이용할 수 있습니다.
import kotlinx.coroutines.*
suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L
        while (i <= 10 && isActive) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
}
fun main() = runBlocking {
    withTimeout(500L) {
        doCount()
    }
}
취소가 되면 TimeoutCancellationException 예외가 발생합니다.
예제 22: withTimeoutOrNull
예외를 핸들하는 것은 귀찮은 일입니다. withTimeoutOrNull을 이용해 타임 아웃할 때 null을 반환하게 할 수 있습니다.
import kotlinx.coroutines.*
suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L
        while (i <= 10 && isActive) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
}
fun main() = runBlocking {
    val result = withTimeoutOrNull(500L) {
        doCount()
        true
    } ?: false
    println(result)
}
성공할 경우 whithTimeoutOrNull의 마지막에서 true를 리턴하게 하고 실패했을 경우 null을 반환할테니 엘비스 연산자(?:)를 이용해 false를 리턴하게 했습니다. 엘비스 연산자는 null 값인 경우에 다른 값으로 치환합니다.
코틀린의 예외는 식(expression)이어 활용이 어렵지는 않습니다만 개인적으로는 null을 리턴하고 엘비스 연산자로 다루는게 더 편한 것 같습니다.