본문 바로가기
Kotlin

코루틴 배우기

by 붕어사랑 티스토리 2021. 12. 29.
반응형

https://kotlinlang.org/docs/coroutines-guide.html

 

Coroutines guide | Kotlin

 

kotlinlang.org

 

 

코루틴 소개

코루틴은 간단히 설명하면 경량화된 스레드 + 다른언어의 async 키워드의 조합이라 생각하면 편하다.

하지만 차이점은 상당히 많다는 점을 기억! 코루틴은 kotlinx.corutines 라이브러리에 담겨있다.

 

 

코루틴의 정의

A coroutine is an instance of suspendable computation. It is conceptually similar to a thread, in the sense that it takes a block of code to run that works concurrently with the rest of the code. However, a coroutine is not bound to any particular thread. It may suspend its execution in one thread and resume in another one.

 

코루틴은 컨셉상 스레드와 비슷하지만, 특정 스레드에 종속되지 않는다. 

A라는 스레드에서 suspend 되었다가, B라는 스레드에서 resume 될 수 있다는 얘기!

 

아래는 대표적인 예제 코드이다

 

import kotlinx.coroutines.*


fun main() = runBlocking {
    launch(Dispatchers.Default) {
        println("Coroutine 1 is running on ${Thread.currentThread().name}")
        delay(1000)
        println("Coroutine 1 is running again on ${Thread.currentThread().name}")
    }
    launch(Dispatchers.IO) {
        println("Coroutine 2 is running on ${Thread.currentThread().name}")
        delay(1000)
        println("Coroutine 2 is running again on ${Thread.currentThread().name}")
    }
}

//결과
Coroutine 1 is running on DefaultDispatcher-worker-1 @coroutine#2
Coroutine 2 is running on DefaultDispatcher-worker-2 @coroutine#3
Coroutine 1 is running again on DefaultDispatcher-worker-2 @coroutine#2
Coroutine 2 is running again on DefaultDispatcher-worker-1 @coroutine#3

 

위 코드를 보면 delay를 통해 코루틴을 중단시키고 다시 시작되었을 때 코루틴이 다른스레드에서 시작된 것을 볼 수 있다.

 

 

 

 

 

코루틴 기초

아래 코드를 돌려보자.

import kotlinx.coroutines.*

fun main() = runBlocking { // 코루틴 scope 시작
    launch { // 새로운 코루틴을 launch한다.
        delay(1000L) // 1초동안 non-blocking delay
        println("World!") // delay 이후 rpint한다
    }
    println("Hello") // 메인 코루틴은 코드를 계속이어나간다. 앞선 코루틴은 1초 딜레이된다
}

 

결과

 

Hello
World!

 

 

자 위의 예제를 설명해 보자

 

 

launch 키워드는 corutine builder, 코루틴 빌더 이다. 이 키워드는 새로운 코루틴을 만들고 코드를 병렬적으로 진행하게된다. 코드가 병렬적으로 진행되기에 Hello 라는 문자가 먼저 출력된 것이다. 그리고 1초후에 World! 가 출력된다.

 

delay 는 suspending function이다. 이 함수는 코루틴을 특정한 시간동안 suspending 시킨다. 허나 다른 하위 코루틴들은 중지 시키지는 않는다.

 

runBlocking 또한  corutine builder, 코루틴 빌더 이다. 그리고 이 함수는 non-corutine 코드와 corutine 코드들의 중간다리 역할을 해 준다.

 

 

 

 

만약 runBlocking 함수를 없애고 launch를 호출하면 코루틴영역이 아닌 곳에서 코루틴을 생성했기에 아래처럼 에러가 난다.

Unresolved reference: launch

 

runBlocking의 의미는 다음과 과 같다. 코루틴 안의 코드를 run 하는동안 / 코루틴을 돌리는 쓰레드는 blocking 된다.

즉 runBlocking 안의 코루틴들이 돌 때 까지, 메인스레드는 기다려야 한다는 뜻.

 

그래서 보통 runBlocking의 경우 어플리케이션의 최상단에 위치하게 된다. 코드레벨 하위단에 있는건 코드가 blocking되고 별로 좋지 않다.

 

 

 

 

 

 

Structured concurrency

코루틴은 Structured concurrency의 원칙을 따른다. 이게 무슨말이냐면 별거없다. 코루틴은 항상 코루틴 scope안에서 생성되어야 한다는 뜻

위 예제에서는 runBlocking이 corutine 영역을 만들고 그 안에서 launch로 새로운 코루틴을 만들었다.

 

 

 

 

 

Suspending Function에 대한 이해

자 여기서 궁금한점이 하나있다. function안에 suspend 저건 무엇을 의미하는가? 한참동안 찾아도 이해하기가 힘들었다. suspend는 중지되고 재개될 수 있는 함수 어쩌구.... 이딴내용만 주구장창 나옴

 

그러다 구글 코루틴 코드랩을 보고 완벽히 이해가 되버렸다. 코드랩에서는 코루틴을 이렇게 소개한다

 

Coroutines are a Kotlin feature that converts async callbacks for long running tasks, such as database or network access, into sequential code.

대충 콜백지옥을 해결 할 수 있다는 얘기...

 

아래의 코드가

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

코루틴을 사용하면 sequentioal 하게 바뀐다

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

 

 

?? 어디서 많이 보던건데... 맞다 자바스크립트의 async와 await이다. 그리고 suspend는 async + await라고 생각하면 된다.

그럼 함수를 async로 명확하게 사용하려면? async 키워드를 붙여주면 된다.

 

 

async는 새로운 코루틴을 생성하며, launch와 다르게 결과값을 리턴할 수 있다. 결과값이 계산되지 않을때는 Defered이다.

 

 

아래와 같은 코드가 있다고 하자

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

 

그리고 아래처럼 계산을 한다

val time = measureTimeMillis {
    val one = doSomethingUsefulOne()
    val two = doSomethingUsefulTwo()
    println("The answer is ${one + two}")
}
println("Completed in $time ms")
The answer is 42
Completed in 2011 ms

대충 2초가 걸렸다. doSomething이 async+await라고 생각하면 확실히 이해가 된다.

그럼 여기서 순순히 자바스크립트처럼 async로 동작하게 async 키워드를 붙이면?

 

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

1초의 시간이 나온다. 웃긴놈들이 async 붙이면 또 job에서 await를 쓸 수 있네...(코루틴은 job이라는걸 리턴한다)

 

 

 

 

 

여기까지 왔다면 코루틴은 아래처럼 세가지로 요약이 가능하다

 

  • 코루틴은 자바스크립트의 async 함수에 대응된다
  • suspending function은 await 의 개념에 똑같다
  • 코루틴안에 또다른 async 작업을 하려면 async 키워드로 새로운 코루틴을 생성한다

 

꼴랑 이 세가지인데 이렇게 쉬운걸 설명을 거지같이 돌려돌려 말하는건 안드로이드 진영의 특인가. 진짜 마음에 안든다.

(이러니 iOS가 진입장벽 있어도 잘나가지 으휴....)

 

 

공식문서를 찾아보니 suspending function은 다른언어의 async와 await를 비슷한 기능이라며 언급하기도 한다.

 

Unlike many other languages with similar capabilities, async and await are not keywords in Kotlin and are not even part of its standard library. Moreover, Kotlin's concept of suspending function provides a safer and less error- prone abstraction for asynchronous operations than futures and promises.

 

 

 

 

Extract function refactoring

자 이제 위의 코드를 리팩토링 해 보자.

메인함수에다가 함수내용을 모두 때려박는 사람은 없을것이다. 따로 함수를 빼 놔서 정리하는게 보통이다.

위 코드는 아래와 같이 정리 될 수 있다.

 

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { doWorld() }
    println("Hello")
}

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

 

launch 안에 코드를 doWorld라는 함수로 따로 빼 내었다. 그리고 여기서 suspend라는 키워드를 사용한 것을 볼 수 있다. 이 suspend 키워드를 사용한 함수를 Suspending function이라고 한다.

 

suspend란 스레드가 잠시 작업을 중단하고 다른작업으로 돌아옴을 의미하는데 자세한 설명은 아래에 적어놨다.

 

Suspending function은 코루틴 안에서 일반 함수처럼 사용가능한 함수이고, Suspending function안에서 다른 Suspending function을 호출 할 수 있다.

 

그리고 Suspending function은 코루틴 안에서만 불려야 한다!

안그럼 아래처럼 에러남

Suspend function 'doWorld' should be called only from a coroutine or another suspend function

 

반대로 일반적인 regular function은 코루틴 안과 밖 모두 불려도 된다.

 

 

 

Scope Builder

 

시작하기전에 용어를 구분하자. launch, aysnc 같은건 코루틴 빌더 이다. 코루틴 영역에서 코루틴을 만드는 함수이다.

스코프 빌더는 코루틴이 실행될 영역을 만드는 빌더이다

 

 

In addition to the coroutine scope provided by different builders, it is possible to declare your own scope using the coroutineScope builder. It creates a coroutine scope and does not complete until all launched children complete.

runBlocking and coroutineScope builders may look similar because they both wait for their body and all its children to complete. The main difference is that the runBlocking method blocks the current thread for waiting, while coroutineScope just suspends, releasing the underlying thread for other usages. Because of that difference, runBlocking is a regular function and coroutineScope is a suspending function.

 

앞서 우리는 corutine scope를 만들기 위해 runBlocking 을 배웠다. 만약 corutine scope안에 corutin scope를 만들고 싶다면? corutineScope 키워드를 이용하면 된다.

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // this: CoroutineScope
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}
Hello
World!

 

 

runBlocking과 corutinScope의 공통점과 차이점은 다음과 같다

 

공통점

두 키워두 모두 하위코드를 실행시키고 하위코드의 작업이 끝날 때 까지 기다린다.

 

 

차이점

1. runBlocking은 regular function이고 corutineScope는 suspend function이다. 즉 corutineScope는 코루틴 스코프 안에서만 불려야한다.

 

2.runBlocking은 기존에 돌고있던 thread를 block 시킨다. corutineScope는 기존에 돌고있던 thread를 suspend 시킨다.

 

 

위 내용을보고 아니 이게 뭔말이여? 할 거다

 

공식문서에 suspend는 이렇게 설명하고 있다.

 

delay is a special suspending function. It suspends the coroutine for a specific time. Suspending a coroutine does not block the underlying thread, but allows other coroutines to run and use the underlying thread for their code.

 

suspend되면 해당 코루틴은 멈추지만, 코루틴을 실행하는 스레드는 다른 코루틴을 실행할 수 있다는 얘기.

한마디로 코루틴이 suspend되어도 다른 코루틴 작업을 할 수 있다는 것이다.

 

반면 blocking은 스레드가 걍 멈춰버린다.

 

 

 

 

아래는 공식문서에서 나온 코루틴 그림

https://kotlinlang.org/docs/coroutines-and-channels.html#starting-a-new-coroutine

Here launch starts a new computation that is responsible for loading the data and showing the results. The computation is suspendable – when performing network requests, it is suspended and releases the underlying thread. When the network request returns the result, the computation is resumed.

Such a suspendable computation is called a coroutine. So, in this case, launch starts a new coroutine responsible for loading data and showing the results.

Coroutines run on top of threads and can be suspended. When a coroutine is suspended, the corresponding computation is paused, removed from the thread, and stored in memory. Meanwhile, the thread is free to be occupied by other tasks:

 

When the computation is ready to be continued, it is returned to a thread (not necessarily the same one).

 

suspend가 delay말고 또 언제 되나 했더니 네트워크 리퀘스트 할 때 된다. 그리고 resume될 때 꼭 같은스레드에서 돌지 않아도 된다.

 

 

 

 

runBlocking의 return 값

아래와 같은 코드는 에러가 난다

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch{
       println("붕어사랑 티스토리♥")
    }
}
No main method found in project.

 

이유는 간단하다. runBlocking 함수는 마지막 statement의 값을 return한다

launch는 Job을 리턴한다. 허나 위의 main함수의 리턴형은 Unit이다. 고로 에러가 난다.

아래와 같이 리턴타입을 Unit으로 명시해주면 해결된다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch{
       println("붕어사랑 티스토리♥")
    }
}

 

 

 

 

An explicit Job

앞서 말한것 처럼 launch는 Job을 리턴한다고 하였다. 아래와 같이 명시적으로 Job의 객체를 받아 launch 스레드를 조작할 수 있다.

val job = launch { // launch a new coroutine and keep a reference to its Job
    delay(1000L)
    println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done")

 

 

 

Corutine Context와 Dispatchers

코루티는 항상 CoroutineContext라는 컨텍스트 내에서 실행이 되어야 한다. CoroutineContext는 하나의 set of various elements로 이루어진다. 그중 mail element는 Job과 dispatcher이다. 그리고 dispatcher에 대해 소개하겠다.

 

 

Dispatcher란 코루틴이 어느 쓰레드, 혹은 쓰레드 풀에서 실행될지를 결정한다. 아래는 Dispatcher을 이용해 스레드를 정해주는 다양한 예시이다.

launch { // context of the parent, main runBlocking coroutine
    println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
    println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher 
    println("Default               : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
    println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
Unconfined            : I'm working in thread main @coroutine#3
Default               : I'm working in thread DefaultDispatcher-worker-1 @coroutine#4
main runBlocking      : I'm working in thread main @coroutine#2
newSingleThreadContext: I'm working in thread MyOwnThread @coroutine#5
  • launch는 호출된곳의 CoroutinScope의 conetext를 상속받아 코루틴을 실행한다
  • Dispatchers.Default는 백그라운드 스레드풀에서 코루틴을 실행한다.
  • newSingleThreadContext는 새로운 스레드를 만들어 코루틴을 실행한다. 이 방법은 비용이 많이 드는 방법으로 실제 사용에 있어서는, 코루틴을 다 썼다면 close를 명시적으로 해주거나. 최상위 변수로 지정해 앱 전체적으로 재활용하며 사용하여야 한다.
  • Dispatchers.Unconfied는 처음에는 suspend 되기 전 까지는 caller의 스레드에서 실행된다. 그러나 suspend 이후 resume 될 때는 어느 쓰레드에서 실행될지는 알 수 없다.

 

 

 

 

Channel

코루틴은 코루틴간의 데이터를 교환하기 위해 Channel이라는 기능이 있다

 

https://kotlinlang.org/docs/coroutines-and-channels.html#channels

 

 

다음은 대표적인 channel 사용 예제이다

val channel = Channel<Int>()
launch {
    for (x in 1..5) channel.send(x * x)
    channel.close() // we're done sending
}
// here we print received values using `for` loop (until the channel is closed)
for (y in channel) println(y)
println("Done!")

채널은 queue와 다르게 close가 가능하다. close는 내부적으로 special한 토큰을 channel에 보내는것과 같다. 그래서 이전에 보내진 데이터들을 확실하게 receive 할 수 있도록 보장받는다.

 

 

채널은 기본적으로 아래와같이 3가지 인터페이스가 있다.

interface SendChannel<in E> {
    suspend fun send(element: E)
    fun close(): Boolean
}

interface ReceiveChannel<out E> {
    suspend fun receive(): E
}

interface Channel<E> : SendChannel<E>, ReceiveChannel<E>
  • SendChannel : 보내기만 가능
  • ReceiveChannel : 받기만 가능
  • Channel : 둘다 가능

 

 

채널은 데이터를 저장하는 방법에따라 또 크게 4가지로 분류된다

 

1. Unlimited Channel

send 되는대로 마다 데이터를 저장한다. send()함수는 suspended되지 않는다. OutOfMemoryException이 발생할 가능성이 있다. Queue와 다른점은, empty channel에서 receive를 하면, 새로운 데이터가 올 때 까지 suspended 된다.

https://kotlinlang.org/docs/coroutines-and-channels.html#channels

 

2. Buffered Channel

채널의 버퍼사이즈가 정해진 채널이다. Producer는 버퍼가 꽉 차기 전 까지 데이터를 보낼 수 있다. 채널이 꽉찬상태에서 send를 호출하면, 채널에 공간이 생길 때 까지 suspended 된다.

https://kotlinlang.org/docs/coroutines-and-channels.html#channels

3. Rendezvous channel

랑데부 채널은 사이즈가 0인 채널이다. send와 receive가 서로가 불릴때 까지 suspended 된다. send와 receive가 서로를 그리워하며 영원히 기다리는 채널이랄까

https://kotlinlang.org/docs/coroutines-and-channels.html#channels

 

4. Conflated Channel

새로운 데이터가 채널로 보내지면, 채널에 있는 이전 데이터를 덮어씌운다. 즉 채널에는 항상 최신의 데이터가 저장된다. send는 절대 suspended 되지 않는다.

https://kotlinlang.org/docs/coroutines-and-channels.html#channels

 

아래는 채널을 만드는 예시. 기본적으로 랑데부 채널이 만들어진다.

val rendezvousChannel = Channel<String>()
val bufferedChannel = Channel<String>(10)
val conflatedChannel = Channel<String>(CONFLATED)
val unlimitedChannel = Channel<String>(UNLIMITED)

 

 

 

 

반응형

댓글