안드로이드/정리

[ Kotlin ] Coroutines을 확실하게

코루틴의 포인트는

이전에 자신의 실행이 마지막으로 중단되었던 지점에서

다음의 장소에서 실행을 재개할 수 있다는 것이다.

 

코루틴은 협력 작업, 예외, 이벤트 루프, 반복자, 무한 리스트, 비동기 처리에 적합하다.

코루틴은 비동기처리를 간단한 코드로 만들어준다. (콜백, 캔슬, 리소스관리 등...)

코루틴은 메인쓰레드가 블라킹되는 부분에 지원을 해준다.

코루틴은 비동기 콜백 처리를 순차적인 코드로 바꿀 수 있다. (콜백지옥으로 되어있는 것을 순차적으로 짤 수 있다.)

 

fun main() {

	GlobalScope.launch {
    	delay(1000L)
        println("World")
    }
    
    print("Hello, ")
    Thread.sleep(2000L)
    println("Hi, ")
}
// Hello, (1초뒤) World (2초뒤) Hi,
// 1000L
// 2000L 이 동시에 실행되는 구조

fun main() {

	thread {
    	Thread.sleep(1000L)
        println("World")
    }
    
    print("Hello, ")
    Thread.sleep(2000L)
    println("Hi, ")
}

// Hello, (1초뒤) World (2초뒤) Hi,
// 위에 코루틴을 사용한 것과 동일하다.

 

runBlocking 사용

runBlocking도 서스펜드로 이루어져있다.

fun main() {

    runBlocking {
        GlobalScope.launch {
            delay(3000L)
            println("World")
        }
        
        print("Hello, ")
        delay(2000L)
    }
}

이 프로그램은 Hello, 만 출력되고 끝난다.

왜냐하면 동시에 진행되어 2000L이 지나고나면 runBlocking안의 내용이 끝나게 되는데

코루틴에서 3000L 뒤에 World를 출력을 시도했기 때문에 World는 출력되지 않는다.

 

fun main() {

    runBlocking {
        val job = GlobalScope.launch {
            delay(3000L)
            println("World")
        }
        
        print("Hello, ")
        job.join()
    }
}

이렇게하면 Hello, World가 성공적으로 출력된다.

GlobalScope.launch(코루틴빌더)가 반환하는 것은 job이라는 객체임을 알 수 있다.

job에 join을 걸면 Coroutine이 완료되기 전까지 기다렸다가 진행된다.

 

위 코드들의 완성형은 다음과 같다.

fun main() {

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

글로벌스코프에서 launch하지 않고, runBlocking에서 들어오는 Coroutine Scope에서 launch를 하면

join을 하지 않아도 launch하는 부분을 기다려준다.

이것을 Structured Concurrency라고 한다.

 

fun main() {
    runBlocking {
        launch {
            myWorld()
        }
        
        print("Hello ")
    }
}

suspend fun myWorld() {

    delay(1000L)
    println("World!!!")
}

// Hello World!!!

 

suspend 키워드가 없으면 myWorld는 delay를 사용할 수 없다.

suspend 키워드가 있어야 중단할 수 있는 펑션이 된다.

 

코루틴은 쓰레드에 비해 매우 가볍다. ( .을 쓰레드와 코루틴으로 각각 10만개 찍어보면 차이를 알 수 있다. )

많은 양의 코루틴을 만들어도 부하가 많지 않다.

 

코루틴을 실행하려면 빌더가 필요하다.

launch, runblocking

 

빌더들은 코루틴 스코프에서 실행된다.

CoroutineScope

GlobalScope (프로그램의 라이프타임 전체를 지원받는 스코프)

 

 

suspend, resume을 시도해보자.

fun main() {

    runBlocking {
        
        launch {
            repeat(5) { i ->
                println("Coroutine A, $i")
                delay(10L)
            }
        }
        
        launch {
            repeat(5) { i ->
                println("Coroutine B, $i")
                delay(10L)
            }
        }
    }
}

Coroutine A, 0

Coroutine B, 0

Coroutine A, 1

Coroutine B, 1

Coroutine A, 2

Coroutine B, 2

Coroutine A, 3

Coroutine B, 3

Coroutine A, 4

Coroutine B, 4

 

A후 10L뒤에 B실행 10L뒤에 A실행 10L뒤에 B실행...

위 처럼 진행된다.

간단하게 suspend와 resume를 체험해볼 수 있는 예시라고 볼 수 있다.

 

 

The Dream Code

val user = fetchUserData()

textView.text = user.name

 

비동기처리를 보다 간편하게 하는 것. (Dream Code)

:: 콜백을 순차적으로 할 수 있는 것

 

Structured Concurrency를 잘 활용하자.

 

fun main() {
    runBlocking {
        val time = measureTimeMillis {
            val one = async { doSomethingUsefulOne() }
            val two = async { doSomethingUsefulTwo() }
            println("The answer is ${one.await()} + ${two.await()}")
        }
        println("Completed in $time ms")
    }
}

suspend fun doSomethingUsefulOne() {
    kotlin.io.println("start, doSomethingUsefulOne")
    delay(3000L)
    kotlin.io.println("end, doSomethingUsefulOne")
    return 1
}

suspend fun doSomethingUsefulTwo(): Int {
    kotlin.io.println("start, doSomethingUsefulTwo")
    delay(3000L)
    kotlin.io.println("end, doSomethingUsefulTwo")
    return 1
}

출력 -

start, one

start, two

end, one

end, two

수행시간 3000ms

 

 

코루틴이 어떻게 중단됐다가 재개됐다가 하는 것일까?
이것은 마법이 아니다.

 

fun postItem(item: Item) {

    val token = requestToken()

    val post = createPost(token, item)

    processPost(post)

}

 

->

 

fun postItem(item: Item) {

    requestToken { token ->

        val post = createPost(token, item)

        processPost(post)
    }

}

 

suspend fun createPost(token: Token, item: Item): Post { ... }

 

코루틴은

JVM으로 들어가며 바이트코드로 컴파일 되면서 다른 변환이 일어난다.

호출했던 함수에 Continuation 이라는 객체를 넘겨주는 형태로 바뀐다.

Suspend가 명시되어있는 함수를 컴파일하면 LABEL이 생긴다. (중단, 재개지점)

코틀린 컴파일러가 내부적으로 이 작업을 한다.

Continuation은 콜백 인터페이스 같은 것.

 

이런 과정으로 코루틴은 중단하고 재개될 수 있게 컴파일된다.

 

 

Dispatchers and threads

모든 코루틴 빌더는 컨텍스트를 줄 수 있다. (어떤 디스패쳐를 사용할 지 전달해주면 됨)

 

Dispatchers.Unconfined - 메인

Dispatchers.Default - DefaultDispatcher(글로벌)

Dispatchers.newSingleThreadContext - 코루틴 실행할 때마다 쓰레드를 만들어 사용 (close를 사용해야함)

Dispatchers.IO - 네트워크 등에 사용