TIL 20240209 - 코루틴(coroutine) ? (3)

2024. 2. 9. 18:17코틀린

우와ㅏㅏㅏ 코루틴 정리하는 것만 삼일째네,, ㅋㅋㅋㅋ

윽 그만큼 내겐 너무나도 어려운 개념이다..

잘 이해할 수 있을까,, 아니야! 난 반드시 해낼거다 후ㅜ 😂 두고봐,, 아잇

그냥 지금으로선 이런거구나 정도만 넘어가고 앞으로 계속 보면서 이해해도록 해보자,,

파이팅

 


완료를 기다리기 위한 블로킹

 

runBlocking의 사용

새로운 코루틴을 실행하고 완료되기 전까지는 현재(caller = 호출자) 스레드를 블로킹한다. 코루틴 빌더와 마찬가지로 CoroutineScope의 인스턴스를 가진다.

fun <T> runBlocking(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> T
): T (source)

 

블록에서 나온 결과물을 돌려주는 식으로 리턴시킨다.

context에 문맥을 지정할 수 있다. 기본값이 EmptyCoroutineContext는 상위에 어떤 코루틴 또는  스코프를 가진 해당 내용에 컨텍스트가 있으면 상속된다. 아니면 직접 소괄호를 사용하면 필요로 하는 문맥을 넣어주면 해당 문맥이 사용된다.

 

 

 

main 스레드 자체를 잡아두기 위해서 블로킹 모드로 동작시키려면 어떻게 해야할까?

fun main() = runBlocking<Unit> { //메인 메서드가 코루틴 환경에서 실행한다.
    launch { //백그라운드로 코루틴을 실행한다.
        dalay(1000L)
        println("World!")
    }
    println("Hello") //즉시 이어서 실행된다.
    //delay(2000L)   //delay()를 사용하지 않아도 코루틴을 기다린다.
}

 

원래는 delay(2000L) 을 넣어줘서 기다리게 한 다음에 했지만 runblocking을 넣어줌으로써 delay가 없어도 코루틴을 기다리게 되는 것이다.. 현재 스레드를 중단함으로써 완료를 기다리는 경우에 유용하게 사용할 수 있다. 

 

 

 

runBlocking은 메인 함수에만 사용 가능 한 것이 아니라

이외에도, 

runBlocking()을 클래스 내의 맴버 메서드에서 사용할 때,

class MyTest {
    fun mySuspendMethod() = runBlocking<Unit>{
        //코드
    }
}

 

 

 

특정 디스패처 옵션을 주어 줄 때,

runBlocking(Dispatchers.IO){
    launch {
        repeat(5)
            println("counting ${it + 1}")
            delay(1000)
    }
}

 

IO는 입출력 동작에 적합한 스레드 옵션으로 설정돼서 구성된다.

launch에 해당 인자가 아무것도 없기 때문에 그대로 해당하는 문맥을 상속받게 된다.

repeat는 해당 내용을 반복하는 것을 말한다.

 

 

 

withContext()

인자로 코루틴 문맥을 지정하며 해당 문맥에 따라 코드 불록을 실행한다. 해당 코드 블록은 다른 스레드에서 수행되며 결과를 반환한다. 부모 스레드는 블록하지 않는다.

(논블로킹과의 차이는 논블로킹은 블록하고 기다리고 있지만,,이는 그렇지 않는다.)

// 함수 원형
suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T (source)

 

예를 들어,

resultTwo = withContext(Dispatchers.IO) { function2() }

 

 

 

또 다른 사용 예로는

완료를 보장하는 것이 있는데,

withContext(NonCancellable) {...}

=try {...} finally {...} 에서 finally 블록의 실행을 보장하기위해 취소불가 블록 구성한다.

 

 

 

coroutineScope 빌더 (Scope Builder)

자신만의 코루틴 스코프(scope)를 선언하고 생성할 수 있다. 모든 자식이 완료되기 전까지는 생성된 코루틴 스코프는 종료되지 않는다. runBlocking과 유사하지만 runBlocking은 단순 함수로 현재 스레드를 블록킹, coroutineScope는 단순히 지연(suspend)함수 형태로 논블로킹으로 사용된다.

즉, runBlocking은 아무것도 안하는 상태이지만 이는 멈춰있어도 뭔가를 하는 상태(다른 요소에서 뭔가를 할 수 있다!)를 일컫는다.

suspend fun <R> coroutineScope (
    block: suspend CoroutineScope.() -> R
): R (source)

 

만일 자식이 코루틴이 실패하면 이 스코프도 실패하고 남은 모든 자식은 취소된다. 반면에 supervisorScope는 실패하지 않는다. 외부에 의해 작업이 취소되는 경우 CancellationException 발생한다.

 

 

 

supervisorScope 빌더 (Scope Builder)

마찬가지로 코루틴 스코프를 생성하며 이때 SupervisorJob과 함께 생성하여 기존 문맥의 Job을 오버라이드 한다.

 

fun SupervisorJob(parent: Job? = null): CompletableJob (source)

 

-launch를 사용해 생성한 작업의 실패는 CoroutineExceptionHandler를 통해 핸들링한다.

-async를 사용해 생성한 작업의 실패는 Deferred.await의 결과에 따라 핸들링한다.

-parent를 통해 부모 작업이 지정되면 자식 작업이 되며 이때 부모에 따라 취소여부가 결정된다.

 

 자식이 실패하더라도 이 스코프는 영향을 받지 않으므로 실패하지 않는다. 실패를 핸드링하는 정책을 구현할 수 있다.

 

예외나 의도적인 최소에 의해 이 스코프의 자식들을 취소하지만 부모의 자체는 취소하지 않는다.

fun main() = runBlocking{ // this: CoroutineScope
    launch {
        delay(200L)
        println("Task from runBlocking")
    }
    coroutineScope { //코루틴 스코프의 생성
        launch {
            delay(500L)
            println("Task from nested launch")
        }
        dalay(100L)
        println("Task from coroutine scope")
    }
    println("Coroutine scope is over")
}

 

 

 

병렬 분해(Parallel decomposition)

suspend fun loadAndCombine(name1: String, name2: String): Image {
    val deferred1= async { loadImage(name1) }
    val deferred2= async { loadImage(name2) }
    return combineImages(deferred1.await(),deferred2.await())
}

 

만약에 이 중에 IoadImage(name1)가 네트워크 불안정 등으로 취소된다면 어떻게 될까?

 

코루틴 빌더가 분리되어있기 때문에 IoadImage(name2)는 여전히 진행된다.

하지만 이는 두 개의 이미지를 병합하는 것으로 하나가 취소되고 하나가 진행되는 것은 의미가 없으므로  더이상 이를 진행할 이유가 없다. 그러므로 실패됐다는 것을 전달하고 그 이후의 것을 진행해야한다.

 

 

코루틴 문맥에서 실행하여 자식 코루틴으로 구성된다면 예외를 부모에 전달하고 모든 자식 코루틴을 취소할 수 있다.

suspend fun loadAndCombine(name1: String, name2: String): Image =
    coroutineScope {
        val deferred1= async { loadImage(name1) }
        val deferred2= async { loadImage(name2) }
        combineImages(deferred1.await(),deferred2.await())
    }

 

coroutineScope로 두 가지를 묶어줌으로서 특정 자식 코루틴의 취소가 생기면 모든 자식을 취소하게 된다.

 

 

스코프 취소의 예로는,

val scope2 = CoroutineScope
val routine1 = scope2.launch {...}
val routine2 = scope2.async {...}

scope2.cancel()
또는
scope2.cancelChildren()

 

변수로 만들어서 필요한 만크의 빌더를 달아준 다음 해당 빌더의 내용을 처리할 수도 있다.

cancel이나 cancelChildren을 하면 모든 하위 자식들을 취소할 수 있다.

try {
    ...
}catch (e: CancellationException){
    ....취소에 대한 예외처리...
}

 

 

 

코루틴의 실행 시간을 지정

 

실행 시간 제한

withTimeout(시간값) {...} - 특정 시간값 동안만 수행하고 블록을 끝난다. 특정완료에 대한 어떤 내용을 받는 것이 아니라 시간값에 대한 내용에 따라서 작동하는 것이다.

시간값이 도달하면 TimeoutCancellationException 예외를 발생한다.

 

withTimeoutOrNull(시간값) {...} - 동작은 위와 동일하지만 예외를 발생하지 않고 null을 반환한다.

val result = withTimeoutOrNull(1300L) {
    repeat(1000) {it ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
    "Done"// 코루틴 블록이 취소가 되면 이 값이 result에 반환된다.
    println("Result is $result")
}

 

repeat 1000번이 반복되면서 하지만 1300L(시간값)이 도달되면 다 처리가 되지 않았더라도 바로 종료되고

코루틴 블록이 취소되면 Done이 result 값에 반환된다. 

null이기때문에 예외에 대한 내용은 처리하지 않고 바로 해당되는 블록이 종료하게 된다.

 

 


악 드디어 끝났다,, 이제 복습하거나 필요한 것이 있으면 추가적으로 학습하는 시간을 가져야지,,

이제 좀만 보다가 쉬자 오늘도 고생했음!!