막무가내 삽질 블로그

코틀린 코루틴 기초 본문

Android

코틀린 코루틴 기초

joong~ 2020. 5. 10. 03:14
728x90

 

 

앱 뿐만 아니라 모든 프로그램에서 비동기 처리는 굉장히 중요하다.

안드로이드에서 여러 비동기를 할 수 있는 메커니즘을 제공하지만 일부 메커니즘에는 굉장히 큰 러닝커브가 있다.(대표적으로 RxJava)

JetBrains에서 코루틴이라는 비동기처리를 할 수 있는 API를 만들었다.

 

코루틴이 나오기 전 가장 핫한 키워드는 rx programming 이였을 것이다. 그러나 구글이 안드로이드 공식 언어를 자바에서 코틀린으로 변경한 이후에 대표적인 샘플 예제들인 blueprint와 snowflower 비동기 처리를 coroutine으로 바꿨다. 코루틴에 대한 영어자료도 최근에 많이 올라 오고 있고 외국 안드로이드 개발자는 코루틴을 많이 사용하고 rx에서 코루틴으로 넘어가고 있다고 들었다.(한국은 rx가 많이 사용중으로 알고 있다)

 

 

코루틴이란?

서로 협력해서 실행을 주고 받으면서 작동하는 여러 서브루틴을 말한다.

 

코루틴의 핵심 키워드

- CoroutineScope, GlobalScope

- CoroutineContext

- Dispatcher

- launch, async

- runBlocking

- Job, Deferred

 

코루틴은 사용할 Dispatcher를 결정하고 Scope를 만들고 laucnch, async에 코드 블록을 넘기면 된다.

 

CoroutineScope : 코루틴의 범위, 코루틴 블록을 제어할 수 있는 단위이다.

GlobalScope : CoroutineScope의 한 종류로 미리 정의된 방식으로 백그라운드에서 동작한다.

CoroutineContext : 코루틴을 어떻게 처리 할 것 인지에 대한 정보의 집합이다.

Dispatcher : CoroutineContext 를 상속받아 스레드를 이용해서 어떻게 동작할 것인지 정의한다.

 - Dispachers.Default : CPU 사용량이 많은 작업에 사용한다. (JVM의 스레드 풀이 지원)

 - Dispachers.IO : 네트워크, 파일 읽기 쓰기...모든 입력 및 출력 작업할 때 사용한다.

 - Dispachers.Main : UI 관련 이벤트를 수행한다. (안드로이드의 경우 메인 스레드는 UI 스레드여서 이 스레드를 사용할 경우 UI 스레드를 사용하여 동작한다)

launch : launch() 함수로 시작된 블록은 Job객체를 반환한다.

async : asynce() 함수로 시작된 블록은 Deferred 객체를 반환한다.

runBlocking : runBlocking() 함수는 작업을 완료하기를 기다린다.

 

 

fun coroutine() {
    GlobalScope.launch { // 백그라운드에서 수행할 launch
        delay(1000L) // non-blocking 1초 기다린다.
        println("World") //  1초 후에 World 출력
    }

    // UI 스레드에서 Hello 출력 후 2초를 기다린다
    println("Hello")
    Thread.sleep(2000L)
}

 

 

코루틴은 굉장히 간결하게 심플하게 사용할 수 있는 코드다.

 

 

GlobalScope.launch {
        delay(1000L) 
        println("World") 
    }

    println("Hello")
    runBlocking { 
        delay(2000L)
    }

runBlocking 함수로 시작된 블록은 해당 블록이 완료될때까지 기다릴 수 있다. 주의해야 할점은 runBlocking 코루틴 블록이 사용하는 스레드는 현재 runBlocking() 함수가 호출된 스레드가 된다는 점이다. 안드로이드의 경우 runBlocking() 함수를 UI 스레드에서 호출하여 시간이 오래 걸리는 작업을 수행하는 경우 ANR이 발생할 수 있어서 주의해야 한다. 

 

 

fun coroutine() = runBlocking {
    val job = GlobalScope.launch {
        delay(3000L)
        println("World")
    }

    println("Hello")

    job.join()
}

 

백그라운드에서 실행한 job객체의 작업이 끝나고 난뒤 (join) UI스레드로 돌아온다.

 

 

 

@RequiresApi(Build.VERSION_CODES.O)
fun now() = ZonedDateTime.now().toLocalTime().truncatedTo(ChronoUnit.MILLIS)

@RequiresApi(Build.VERSION_CODES.O)
fun log(msg: String) = println("${now()}: ${Thread.currentThread()}: $msg")

@RequiresApi(Build.VERSION_CODES.O)
fun globalScope() {
    GlobalScope.launch {
        log("coroutine start")
    }
}

@RequiresApi(Build.VERSION_CODES.O)
fun main() {
    log("main() start")
    globalScope()
    log("globalScope executed")

    Thread.sleep(5000L)
    log("main() terminated")
}

GlobalScope는 메인 스레드가 실행 중인 동안만 코루틴의 동작을 보장해 준다. Thread.sleep을 없애면 코루틴이 아예 실행되지도 않는다. 

 

 

fun main() = runBlocking {
    GlobalScope.launch {
        repeat(1000) { i ->
            println("sleeping $i")
            delay(500L)
        }
    }

    delay(5000L)
}

문자열 1000회를 0.5초마다 출력하고 메인 쓰레드는 5초를 기다린다. 9까지 출력될 것이다. 이유는 GlobalScope는 메인 스레드가 실행중일 때만 동작을 보장해주기 때문...

 

 

fun main() = runBlocking {
    val job = launch {
        repeat(500) { i ->
            println("ln $i")
            delay(300L)
        }
    }

    delay(3000L)
    job.cancel()
}

3초가 지나면 job 객체를 취소해 준다. 항상 실행을 했으면 종료를 해주는 쪽이 좋다(메모리관련)

 

Comments