[Coroutine] delay

코틀린 코루틴을 공부하며 정리한 글입니다.
혼자 공부하고 정리한 내용이며, 틀린 부분은 지적해주시면 감사드리겠습니다 😀

책에서 자세하게 다루지는 않지만, 이전 포스팅에서 본 Thread.sleep과 중단 함수에서 사용 가능한 delay는 과연 어떤 차이가 있길래 단일 스레드 환경에서 차이가 나는걸까?

🛠️ 개발 환경 및 테스트 환경

  • kotlin : 1.9.24
  • Spring Boot : 3.3.1

Thread

이 개념을 이해하기 위해서는 우선 Thread란 무엇인지 알아야 한다. 간단하게 스레드는 프로세스 안에서 작업을 진행하는 작업자라고 생각하면 된다.

1시간에 손님이 100명 방문하는 카페가 있는데, 알바생이 1명이면 어떨까? 음료를 주문하고, 그 음료를 받기까지 시간이 오래 걸릴 것이고, 알바생도 힘들 것이다. 때문에 여러 명의 알바를 고용해서 업무를 진행한다. 여기서 카페가 Process이며, 손님은 Request, 알바생이 바로 Thread라고 생각하면 편하다.

sleep()

이제 Thread에 대해서 간단한 지식을 가졌으니, sleep은 과연 뭐하는 함수인지 확인해보자.

Thread.sleep(millseconds) 함수는 현재 스레드를 지정한 시간 동안 블로킹을 한다. 즉, 해당 스레드는 지정된 시간이 지날 때까지 아무런 작업도 할 수 없는 수면 상태에 빠지는 것이다.

앞서 본 비유를 그대로 이용해서 보면, 알바생이 업무 시간에 지정한 시간동안 잠을 자는 것이다.

fun main() {  
    println("[${Thread.currentThread().name}] Before sleep")  
    Thread.sleep(1000)  
    println("[${Thread.currentThread().name}] After sleep")  
}
[main] Before sleep (54.053s)
[main] After sleep (55.077s)

위 코드를 실행시켜보면 Before sleep이 출력되고 1초 뒤에 After sleep이 출력되는 것을 볼 수 있다. 즉, main 함수를 실행하는 main 스레드가 아예 잠에 들어서 이후 코드를 진행하지 않게 되는 것이다.

delay()

그렇다면 delay()를 사용했을 때는 어떨까? 결과를 먼저 확인해보자.

suspend fun main() = coroutineScope {  
    println("[${Thread.currentThread().name}] Before delay")
    launch {  
        delay(1000L)  
    }  
    println("[${Thread.currentThread().name}] After delay")  
}
[main] Before delay (8.319s)
[main] After delay (8.348s)

시간을 보면 알 수 있듯, 모든 println이 먼저 출력되고, 1초 뒤에 프로그램이 종료된다. delay(1000L)의 앞, 뒤로 로그를 넣어서 보면 다음과 같은 결과가 나오게 된다.

[main] Before delay (8.319s)
[main] After delay (8.348s)
1
2
[main] Before delay (8.319s)
1
[main] After delay (8.348s)
2

위처럼 2가지 결과가 랜덤으로 나오는데, 이는 콜백 함수가 비동기적으로 처리되기 때문에 launch 블록이After delay가 출력이 되기 전에 실행이 될 수도 있고, After delay가 출력이 되고 실행이 될 수 있기 때문이다.

coroutineScope, launch는 이후 포스팅에서 다룰 예정입니다.

다시 본론으로 돌아와서, 이전 포스팅에서 본 것과 같이 delay를 쓴 것 만으로도 단일 스레드에서 모든 요청을 바로바로 처리한 것을 확인할 수 있었는데, 어떻게 이런 일이 가능한 것일까? 이는 delay 함수의 구현을 살펴보면 알 수 있다.

public suspend fun delay(timeMillis: Long) {  
    if (timeMillis <= 0) return // don't delay  
    return suspendCancellableCoroutine { cont: CancellableContinuation<Unit> ->  
        // if timeMillis == Long.MAX_VALUE
        // then just wait forever like awaitCancellation, don't schedule.
        if (timeMillis < Long.MAX_VALUE) {  
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)  
        }  
    }  
}
public suspend inline fun <T> suspendCancellableCoroutine(  
    crossinline block: (CancellableContinuation<T>) -> Unit  
): T = suspendCoroutineUninterceptedOrReturn { uCont -> 
    val cancellable = CancellableContinuationImpl(
        uCont.intercepted(), resumeMode = MODE_CANCELLABLE
    )  
    /*  
    * For non-atomic cancellation we setup parent-child relationship immediately
    * in case when `block` blocks the current thread
    * (e.g. Rx2 with trampoline scheduler),
    * but properly supports cancellation.
    */
    cancellable.initCancellability()  
    block(cancellable)  
    cancellable.getResult()  
}

delay는 스레드를 재우는 것이 아닌, suspendCancellableCoroutine 내부에 있는 uCont.intercepted()을 통해 현재 코루틴을 중단(suspend)시키고, 특정 작업이 완료될 때까지 다른 코루틴의 작업을 처리하면서 기다리게 된다.

앞서 본 비유를 그대로 이용해서 보면, 스무디 주문이 들어오면 블렌딩을 하는 동안 다음 커피 주문을 받고, 샷을 내리는 동안 블렌딩 완료된 음료를 준비해서 손님에게 전달하고, 샷을 다 내렸는지 확인하고, 아직 진행 중이라면, 다음 주문을 받고, 다 내려진 샷을 준비해서 손님에게 전달하는 형태이다.

즉, 중단 함수를 사용하는 것은 이 함수가 중단된 동안 다른 일을 처리하지만, 계속해서 앞선 작업을 확인하고, 처리하는 형태인 것이다.

정리

Thread.sleep은 현재 작업을 처리하는 스레드를 재워 블로킹을 하는 것이고, delay는 스레드를 블로킹하는 것이 아닌, 중단(suspend)를 시키고, 다른 작업을 처리하는 것이다. 다른 작업을 처리하는 중에 중단이 일어나면, 다시 기존 작업을 처리하러 돌아온다.