Android/Coroutine

[Kotlin Coroutines][init] 3. 코루틴 중단(suspend)하고 재개(resume)하기

네모메모 2024. 2. 18. 15:14
반응형

 

 

 

 

 

 

 

 

 

코틀린 코루틴은 '비동기적인 작업을 수행할 때 사용되는 경량 스레드'입니다.

 

코루틴은 실행 중인 함수의 실행을 일시 중단하고 나중에 재개할 수 있는 기능을 제공하여

비동기적인 작업을 보다 효율적으로 관리할 수 있습니다.

 

이러한 일시 중단과 재개는 코루틴의 핵심 개념 중 하나인 "중단 함수"를 통해 이루어집니다.

 

 


 

 

중단 함수(suspend function)

  • 코루틴을 중단 할 수 있는 함수 (= 코루틴에서 함수의 실행을 일시 중단하고 나중에 재개할 수 있는 기능을 제공하는 함수)
  • 코루틴을 중단할 수 있으므로 중단 함수는 반드시 코루틴(또는 다른 중단함수)에 의해 호출되어야 한다.

 

  • 중요하다 (코틀린 코루틴의 핵심적인 개념)
  • [비유] 비디오 게임을 하다가 멈추는 상황이랑 비슷하다. 체크 포인트에서 게임을 저장하고 종료한 뒤, 사용자와 컴퓨터는 각자의 일을 하다가 다시 게임을 재개하고 저장한 체크포인트에서 시작할 수 있다. 이는 코루틴의 철학과 비슷하다.
  •  [사용] 'suspend 키워드'를 함수에 추가
  • 코루틴은 중단되었을때 'Continuation 객체'를 반환한다.
    • 'Continuation 객체'를 이용해 중단된 곳에서 다시 코루틴을 실행(='재개(resume)')할 수 있다.
      (<< 이 객체는 게임을 저장하는것과 같다.)
      • 'Continuation 객체'는 (이론상) 직렬화와 역직렬화가 가능하며 다시 '재개(resume)'될 수 있습니다.
      • cf) 이 점에서 코루틴은 스레드 다르며 훨씬 강력하다. 
              > 스레드는 저장이 불가능하고 멈추는 것만 가능하다.
              > 코루틴은 중단 되었을 때 코루틴은 어떤 자원도 사용하지 않으며 재개가능하다.

 

  • [중단시키기]
    • #1) suspendCoroutine함수를 사용
      • suspendCoroutine 람다 표현식 안에는 'Continuation 객체'를 인자로 받는데
        • 위 람다 표현식은 중단하기 전 실행된다.
        • 'Continuation 객체'를 저장한 뒤 코루틴을 재개(resume) 시점을 결정하기 위해 사용된다.
      • ex1) before와 after의 중간에 중단하기 위해 'suspendCoroutine함수'를 적용한 코드
suspend fun main()  {
    println("Before")
    
    suspendCoroutine<Unit> { continuation ->  
        println("Before too")
    }
    
    println("After")
}
//Before
//Before too

 

 


재개(resume)

  • 작업을 재개(resume)하려면 '코루틴'이 필요하다.
    • '코루틴'은 코루틴빌더(runBlocking, launch 등)을 통해 생성됨
  • [재개 방법] 재개할 코루틴이 중지될 때 전달한 'Continuation 객체'의 resume()함수를 호출합니다.
    • ex1-1) 코루틴을 재개해서 after를 호출하려면?
suspend fun main()  {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
        continuation.resume(Unit)
    }

    println("After")
}
//Before
//After

   ㄴ> 실제 환경에서 곧바로 재개하면 최적화로 인해 아예 중단을 시키지 않을 수도 있습니다.

 

 


다른 스레드에서 재개(resume)하기

  • 코루틴을 이해하는 데 중요한 방식
  • ex1의 재개를 다른 스레드에서 하고 싶다면?
    • ex1-2a) Thread.sleep을 호출
suspend fun main()  {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
        thread { 
            println("Suspended")
            Thread.sleep(1000)
            continuation.resume(Unit)
            println("Resumed")
        }
    }

    println("After")
}
//Before
//Suspended
//(1초뒤)
//After
//Resumed

   ㄴ> 잠깐 동안 정지(sleep) 된 뒤 재개되는 다른 스레드를 실행함

   ㄴ> 대기할 때마다 하나의 스레드를 블로킹하는 방법으로 좋지는 않음

 

    • 코루틴의 delay 함수 사용을 권장

- [코루틴 delay 함수의 구현]

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { 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)
        }
    }
}

 

 

 


 

값으로 재개(resume)하기

- 코루틴은 값으로 재게하는것이 자연스럽다. 

suspendCoroutine은 호출될때 'Continuation 객체'로 반환될 값의 타입을 지정할 수 있다.
ex) suspendCoroutine<User>
이후 'Continuation 객체'.resume() 함수에 전달하는 인자값은 반드시 지정된 타입과 같은 타입이어야 한다.
ex) cont.resume(user)

 

- 이를 활용하면 API를 호출해 네트워크 응답을 기다리는 것처럼 특정 데이터를 기다리려고 중단하는 상황을 자주 발생한다.

스레드는 특정 데이터가 필요한 지점까지 비지니스 로직을 수행한다. 이후 네트워크 라이브러리를 통해 데이터를 요청한다.

코루틴이 없다면 스레드는 응답을 기다리고 있을 수밖에 없다.

이렇게 되면 엄청난 낭비이다.

그러나 코루틴이 있으면 중단함과 동시에 "데이터를 받고 나면, 받은 데이터를 resume 함수를 통해 보내줘."라고 컨티뉴에이션 객체를 통해 라이브러리에 전달 한다. 그러면 스레드는 다른 일을 할 수 있다. 그리고 데이터가 도착하면 스레드는 코루틴이 중단된 지점에서 재개된다.

 

- ex2-1) 외부에 구현된 requestUser 콜백 함수를 사용

suspend fun main()  {
    println("Before")

    val user = suspendCoroutine<User> { cont ->
        requestUser { user ->
            cont.resume(user)
        }
    }
    println(user)
    println("After")
}
// Before
// (1초뒤)
// User(name=Tapas, age=30)
// After

 

 

- suspendCoroutine을 직접 호출 하는것은 불편하다. 대신 중단 함수를 호출하는 것이 낫다.

suspend fun requestUser(): User {
    return suspendCancellableCoroutine<User> { cont ->
        requestUser {
            cont.resume(it)
        }
    }
}

suspend fun main()  {
    println("Before")
    val user = requestUser()
    println(user)
    println("After")
}

 

 

 

위와 같이

중단함수 내에서 콜백함수를 사용하는 일

- 위 예제와 같이 중단함수내에서 콜백함수를 사용할 일은 거의 없다. 
  (중단함수는 Room과 Retrofit 같은 널리 사용되는 라이브러리에 의해 지원되고 있기 때문에)

- 만약 필요 시 suspendCoroutine 대신 suspendCancellableCoroutine을 사용하는 것이 좋다.

 

 

여기서 중요한 점은 함수가 아닌 코루틴을 중단시키는 점이다. 

 

 

 


 

 

 

모든 함수는 값을 반환하거나 예외를 던진다. 

suspendCoroutine 또한 마찬가지이다.

suspendCoroutine이 예외를 던지는 경우는 어떻게 재개할까?

 

 

예외(exception)으로 재개(resume)하기

  • 'Continuation 객체'.resumeWithException이 호출되면 중단된 지점에서 인자로 넣어준 예외를 던진다.  
      cf) 'Continuation 객체'.resume이 호출될 때, suspendCoroutine은 인자로 들어온 값을 반환한다.
  • 이는 위의 API를 호출해 네트워크 응답을 기다리는 것처럼 특정 데이터를 기다리려고 중단하는 상황 같은 상황에서
    네트워크 예외를 알릴 때 매우 유용하다
  • ex)
suspend fun requestUser(): User {
    return suspendCancellableCoroutine<User> { cont ->
        requestUser { resp ->        
            if (resp.isSuccesful) {
                cont.resume(resp.data)
            } else {
                val e = ApiException(
                    resp.code,
                    resp.message
                )
                cont.resumeWithException(e)
            }
        }
    }
}


suspend fun requestNews(): News {
    return suspendCancellableCoroutine<User> { cont ->
        requestNews(
            onSuccess = { news -> cont.resume(news) }
            onFailure = { e-> cont.resumeWithException(e) }
        )
    }
}

 

 

 


 

 

이 외 알아두고 주의할 점들

"중단함수는 코루틴이 아니고, 단지 "코루틴을 중단"할 수 있는 함수이다!"

 

 

  • Ex) 변수에 Continuation 객체를 저장하고, 함수를 호출한 다음에 이 변수를 이용해서 코루틴을 재개하면? 
    ㄴ> [위험] 종료되지 않습니다, 
    continuation 프로퍼티에 Continuation 객체를 저장해두고, 계속해서 일시정지되어 있는 상황으로
    메인함수에서 suspendAndSetContinuation()이후 코드는 실행되지 않습니다.
// 이렇게 구현하면 절대 안됩니다.
var continuation : Continuation<Unit>? = null

suspend fun suspendAndSetContinuation() {
    suspendCoroutine<Unit> { cont ->
        continuation = cont
    }
}

suspend fun main() {
    println("Before")

    suspendAndSetContinuation()
    continuation?.resume(Unit)

    println("After")
}
// Before

 

 

 

"다른 스레드나 다른 코루틴으로 재개(resume)하지 않으면 프로그램은 실행된 상태로 유지됩니다."

 

  • Ex2) 그렇다고 아래 코드👇 처럼 작성하지 마세요.
    ㄴ> [위험] 종료는 되지만 메모리 누수가 발생할 수 있습니다.
// 이렇게 구현하면 절대 안됩니다.
var continuation : Continuation<Unit>? = null

suspend fun suspendAndSetContinuation() {
    suspendCoroutine<Unit> { cont ->
        continuation = cont
    }
}

suspend fun main() = coroutineScope {
    println("Before")

    launch {
        delay(1000L)
        continuation?.resume(Unit)
    }

    suspendAndSetContinuation()
    println("After")
}

// Before
// (1초 후)
// After

 

 

 

 


 

출처


- [Book] 코틀린 코루틴 : 안드로이드 및 백엔드 개발자를 위한 비동기 프로그래밍 마르친 모스카와 저 / 신성열 역 | 인사이트(insight) | 2023년 11월 01일

출처: https://nemomemo.tistory.com/229 [nemo's dev memos:티스토리]

 

 

 

 

 

 

반응형