본문 바로가기
2023년 이전/kotlin

코루틴(Coroutine)이란 -2

by JeongUPark 2020. 9. 13.
반응형

코루틴(Coroutine)이란 -1 을 보고 오시면 이해가 더 좋습니다.

 

이전 글에서 launch aysnc runblock및 기본 코루틴 동작 방법을 공부해 봤습니다. 그럼 이번에는 추가로 코루틴 사용 방법에 대해서 익혀 보겠습니다.

 

runBlack 활용

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main(){
    println("Start")
// Start a coroutine
    GlobalScope.launch {
        delay(3000)
        println("Hello")
    }
    runBlocking {
        delay(2000)
    }
    println("Stop")

}

이렇게 runBlocking을 사용하여 main Thread에 2초 딜레이를 줄 수 있었습니다. 이 코드는 다음과 같이 변경이 가능 합니다.

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking<Unit> {
    println("Start")
// Start a coroutine
    GlobalScope.launch {
        delay(3000)
        println("Hello")
    }
    delay(2000)
    println("Stop")
}

여기서 runBlocking<Unit>는 최상위 레벨의 메인 코루틴을 시작하는데 사용하는 어댑터로서 일합니다. 그리고 반환형을 명시적으로 unit로 지정하였는데, 코틀리네서 잘 구성된 main 함수는 반환값으로 unit를 가지기 때문입니다.

 

Waiting for a job

코루틴에서 다른 코루틴의 동작하는 동안 잠시 지연하는 방법은 좋은 접근이 아닙니다. 우리가 동작 시킨 백그라운드에서 동작하는 Job이 완료 될때가지 명시적을 기다려야 합니다. 그 당법은 다음과 같습니다.

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {  
    val job = GlobalScope.launch { 
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // wait until child coroutine completes
}

이렇게 .join을 통하여 launch{}가 완료 될 때까지 명시적으로 기다립니다. (위 코드에서 만일 job.join이 없다면.. 결과는 Hello,만 나오고 끝낫을 것입니다.) 이렇게 되면 메인 코루틴의 코드는 백그라운드의 어떤 작업을 하든 그 기간과 관련없이 동작할 수 있습니다.

 

구조화된 동시성

코루틴이 스레드에 비해 훨씬 적응양의 메모리를 사용한다는 것을 알고 있습것입니다. 하지만 그런 코루틴도 동작할때 메모리를 아예 사용하지 않는 것은 아닙니다. 새로 시작된 코루틴을 위한 레퍼런스 유지를 잊어도 , 이는 계속 동작할 것입니다. 그리고 코루틴 코드가 멈춘다면( 예를 들어 장시간 지연될 경우) , 또는 메모리를 초과할 정도로 많은 코루틴이 동작한다면, 실행 된 모든 코 루틴에 대한 참조를 수동으로 유지하고 조인해야하는 것은 오류가 발생하기 쉽습니다.

 

그래서 더나은 방법으로 , 우리는 코드에  구조화된 동시성을 사용할 수 있습니다. GlobalScope 에서 동작하는 코루틴 대신에 우리가 수행하는 작업의 특정 범위에서 코 루틴을 시작할 수 있습니다.   ( 1.GlobalScope는 일반 적으로 Thread와 함께 동작합니다. 그 이유는 thread는 항상 global(전역) 이기 때문입니다. 2. 그리고 GlobalScope는 범위가 없는 코루틴 scope를 말합니다.) 

 

모든 코루틴 빌더는 , 코루틴 블럭 범위에서 코루틴 scope 인스턴스를 추가 합니다. 그래서 외부 코루틴 (이 예에서는 runBlocking)이 해당 범위에서 시작된 모든 코 루틴이 완료 될 때까지 완료되지 않기 때문에 명시 적으로 join하지 않고도이 범위에서 코루틴을 시작할 수 있습니다.

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

Scope builder

다른 빌더가 제공하는 코루틴스코프 이외에도, coroutineScope builder를 사용하여 여러분의 자신의 scope를 알릴 수 있습니다. 이는 코루틴스코프를 만들고 동작된 모든 자식들이 완료될 때까지 완료되지 않습니다. 

runBlocking과 coroutineScope는 Body와 자식들이 완료될 떄까지 기다린다는 점에서 동일 합니다. 다른 점은 runBlocking은 기다리기 위해 현재 Thread를 block하는 반면 coroutineScope는 다른 사용을 위해 기본 스레드를 해제하고 일시 중지 합니다. 이런 차이 때문에  runBlocking은 일반 함수이고 coroutineScope는 일시 중지 함수입니다.

import kotlinx.coroutines.*

fun main() = runBlocking { 
    launch {
        delay(200L)
        println("Task from runBlocking")
    }

    coroutineScope {
        launch {
            delay(500L)
            println("Task from nested launch")
        }

        delay(100L)
        println("Task from coroutine scope")
    }

    println("Coroutine scope is over")
}

함수 리팩토링 추출(Extract function refactoring)

자 그럼 launch{ } 안의 코드를 함수로 변경해 보겠습니다.(이미 코루틴이란?-1에서 다뤘을니 빠르게 넘어가겠습니다!)

 

launch{} 안에 들어갈 함수를 만듭니다.

fun doWorld() {
    delay(1000L)
    println("World!")
}

그런데 작성해보면 delay에서 에러가 납니다. 그 이유는 일반 함수가 아니 코루틴에서 사용할 수 있기 때문입니다. 그래서 fun 앞에 suspend를 붙이면 사용이 가능해집니다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

이렇게 만든면 코루틴에서 함수로 사용할 수 있습니다.

 

만약에 추출 된 함수에 현재 범위에서 호출되는 코 루틴 빌더가 포함되어 있다면 어떻게 될까요? 이에 대한 해결책은 여기서 확인해보세요. (이게 제가 글을 쓰는것보다 먼가 원서를 읽어보시는게 더 정확할 것이라 판단이 되어 이렇게 처리 하였습니다.)

 

글로벌 코루틴은 데몬 스레드와 같습니다. (Global coroutines are like daemon threads)

구조화된 동시성에서 이미 한번 언급하였지만 글로벌 코루틴은 기본 스레드와 비슷합니다. 다음 코드를 보면

import kotlinx.coroutines.*

fun main() = runBlocking {
    GlobalScope.launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // just quit after delay
}

I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... 이렇게 프린트 하고 종료됩니다. 위 코드는 GlobalScpoe에서 "I'm sleeping"을 1초에 두번 프린트 한 다음 약간의 지연 후 주 함수에서 반환하는 장기 실행 코루틴을 시작합니다.

 

GlobalScope에서 시작된 활성 코루틴은 프로세스를 활성 상태로 유지하지 않습니다. 이는 데몬 스레드와 같습니다.

이를 확실하게 이해하기 위해 다음 두 코드를 비교해 보겠습니다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    GlobalScope.launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        print("hello")
    }
}

위 코드의 결과는 I'm sleeping 0 ... 입니다.

반면

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        print("hello")
    }
}

이 코드는 I'm sleeping 0~ 1000까지 나오고 끝에 hello가 나타나면서 종료 합니다.

위에서 말한 "GlobalScope에서 시작된 활성 코루틴은 프로세스를 활성 상태로 유지하지 않습니다. 이는 데몬 스레드와 같습니다."가 조금 이해가 되셨을까요? ㅎ

 

 

참조

kotlinlang.org/docs/reference/coroutines/basics.html

 

Basics - Kotlin Programming Language

 

kotlinlang.org

 

반응형