취소와 타임아웃
예제 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
을 리턴하고 엘비스 연산자로 다루는게 더 편한 것 같습니다.