Backend/Kotlin

Coroutine timeout 설정

findmypiece 2022. 2. 4. 09:40
728x90

Coroutine 을 사용하는 이유는 병렬처리 또는 자원의 균일한 사용이다. 이런 면에서 timeout 은 필수로 필요한 설정이 된다. Coroutine 패키지에서 제공되는 withTimeout 함수를 사용하면 손쉽게 timeout 을 설정할 수 있는데 이걸 어떤 형태로 사용하느냐에 따라 동작이 전혀 달라지기 때문에 이를 기록해 놓는다.

 

결론부터 말하면 아래와 같은 형태로 사용하면 된다.

runBlocking(Dispatchers.IO) {
    repeat(5){
        launch {
            try{
                withTimeout(3000) {
                    if (it == 1)
                        delayRoutine(it)
                    else
                        normalRoutine(it)
                }
            }catch (e: Exception){
                println("$it ${e.message}")
            }
        }
    }
}

suspend fun delayRoutine(idx: Int){
    println("$idx delayRoutine routine Start...")
    delay(7000)
    println("$idx delayRoutine routine End...")
}

fun normalRoutine(idx: Int){
    println("$idx normalRoutine routine Start...")
    println("$idx normalRoutine routine End...")
}

 

 

3개의 routine 을 병렬로 수행하는 테스트 코드이다. repeat 구문에 의해 0~2의 index를 가지는 3개의 loop가 수행될 것이고 중간에 해당하는 1 index 에 해당하는 작업만 3초의 딜레이를 줬다. 

 

위 테스트 수행결과는 아래와 같다. 우리가 기대한 데로 각각의 launch 작업들은 논블로킹으로 병렬 수행되었고 그 중 delayRountine 작업은 타임아웃으로 취소 되었다. 

2 normalRoutine routine Start...
1 delayRoutine routine Start...
0 normalRoutine routine Start...
2 normalRoutine routine End...
0 normalRoutine routine End...
4 normalRoutine routine Start...
4 normalRoutine routine End...
3 normalRoutine routine Start...
3 normalRoutine routine End...
1 Timed out waiting for 3000 ms

 

위 테스트를 여러번 반복적으로 실행해보면 아래 출력결과가 간혹 달라지는 것을 확인할 수 있고 이를 통해 각각의 launch 작업들이 논블로킹으로 수행되었다는 것을 알 수 있다.

 

이제 잘못된 사용법을 살펴보자. 참고로 withTimeout 은 CoroutineScope 안에서만 정의할 수 있고 지정된 시간동안 작업이 종료되지 않으면 TimeoutCancellationException 을 발생시킨다.

 

그렇다면 CoroutineScope 는 runBlocking(Dispatchers.IO) 단계에서 이미 생성되기 때문에 runBlocking(Dispatchers.IO) launch  사이에도 withTimeout 을 지정할 수 있다는 말이 되고 실제로 아래와 같이 사용이 가능하다.

@Test
fun timeoutTest2(){

    runBlocking(Dispatchers.IO) {
        repeat(5){
            try{
                withTimeout(3000) {
                    launch {
                        if (it == 1)
                            delayRoutine(it)
                        else
                            normalRoutine(it)
                    }
                }
            }catch (e: Exception){
                println("$it ${e.message}")
            }
        }
    }
}

 

위 테스트의 실행결과는 아래와 같다.

0 normalRoutine routine Start...
0 normalRoutine routine End...
1 delayRoutine routine Start...
1 Timed out waiting for 3000 ms
2 normalRoutine routine Start...
2 normalRoutine routine End...
3 normalRoutine routine Start...
3 normalRoutine routine End...
4 normalRoutine routine Start...
4 normalRoutine routine End...

 

그런데 이렇게 되면 우리가 기대한 것과는 좀 다르게 동작한다. launch 작업들이 모두 블로킹으로 순차실행 되었다. 우리가 기대했던 병렬로 작업되지 않았다. 테스트를 여러번 실행해봐도 결과는 항상 동일하다. 

 

CoroutineScope 내부에 정의하는 launch, async 블럭은 기본적으로 논블로킹으로 수행되지만 withTimeout 으로 감싸는 순간 블로킹 수행으로 바뀌어 버린다.

 

이런 이유로 아래와 같이 try...catch 절까지 제거해버리면 Exception 이 발생할 경우 코루틴이 아예 중단되고 이후 routine 들은 아예 실행되지 않는다. (withTimeout 를 launch 내부에 정의할 경우 해당 routine만 실패한다)

@Test
fun timeoutTest3(){

    runBlocking(Dispatchers.IO) {
        repeat(5){
            withTimeout(3000) {
                launch {
                    if (it == 1)
                        delayRoutine(it)
                    else
                        normalRoutine(it)
                }
            }
        }
    }
}
0 normalRoutine routine Start...
0 normalRoutine routine End...
1 delayRoutine routine Start...

Timed out waiting for 3000 ms
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 3000 ms

 

일반적으로 withTimeout 를 launch, async 내부에 정의하는 방식이 사용될 것 같은데 이러한 차이점을 정확히 알고 적절하게 사용해야 한다.

728x90