Coroutine 기초

조갱·2022년 3월 27일
0

Coroutine

목록 보기
2/9

공식 문서 링크 : https://kotlinlang.org/docs/coroutines-basics.html

내가 직접 학습하면서 올리는 코드 : https://github.com/KyungHyun-Cho/Coroutine-Ex

최종 수정일: 2021-09-02
이 섹션은 coroutine의 기본 개념에 대해 다룹니다.

번역본

당신의 첫번째 coroutine

coroutine은 정지할 수 있는 연산의 인스턴스이다. coroutine은 코드 블럭을 다른 코드와 동시적으로 실행할 수 있다는 점에서 개념적으로 thread와 비슷하다. 그러나 coroutine은 다른 어느 특정한 thread와 바인딩되지 않는다. coroutine은 하나의 thread에서 실행을 중단하고, 다른 것을 계속 (작업)한다.

coroutine은 경량 스레드로서 생각될 수 있지만, 실생활에서 thread와는 매우 다르게 사용되는 많은 중요한 차이점들이 존재한다.

첫 번째 coroutine 실습을 위해 아래 코드를 실행해보자.

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello") // main coroutine continues while a previous one is delayed
}

전체 코드 : 이곳을 클릭
아래와 같은 결과를 확인할 수 있다.

Hello
World!

이 코드가 무엇을 하는지 분석해보자.

launch는 coroutine builder이다. 이것은 새로운 코루틴을 나머지 코드와 동시적으로 실행시키는데, 이는 독립적으로 계속 동작한다. 이것이 Hello가 먼저 출력된 이유이다.

delay는 특별한 중단 함수이다. delay는 특정 시간동안 coroutine을 중단시킨다. coroutine을 중단하는 것은 기본 thread를 중단시키진 않지만, 다른 coroutine을 실행되어 해당 코드의 기본 thread를 사용할 수 있게 한다.

runBlocking 또한 runBlocking {...}에서 중괄호 사이의 코루틴 코드와 fun main() 과 같은 일반적인 non-coroutine 세계와 연결해주는 coroutine builder이다. 이것은 IDE에서 runBlocking을 여는 중괄호 바로 오른쪽에서 this: CoroutineScope와 같이 표시된다.

만약 당신이 이 코드에서 runBlocking을 까먹거나 제거한다면 launch를 호출할 때 에러가 발생하는데,launch는 오직 CoroutineScope에서만 선언될 수 있기 때문이다.

Unresolved reference: launch

runBlocking 이름이 의미하는 것은, runBlocking을 실행한 threadrunBlocking {...} 내의 coroutine 구문의 실행이 완료될 때까지 멈춘다는 것이다. (이 예제에서는 main thread). 당신은 runBlocking이 최상위 수준의 application에서 사용되고, 실제 코드에서는 거의 사용되지 않는 것을 자주 볼 수 있는데, thread는 고가의 리소스이며 thread를 blocking 하는 것은 비효율적이고 바람직하지 않은 경우가 많이 때문이다.

Structured concurrency

coroutine은 structured concurrecy의 원리를 따른다. 즉, 새로운 coroutine은 오직 coroutine의 lifetime을 결정짓는 특정한 CoroutineScope에서만 실행될 수 있다는 것을 의미한다. 위 예시는 runBlocking이 해당 스코프를 설립하기 때문에 1초 지연 후 World!가 출력되기까지 기다렸다가 종료되는 것을 보여준다.

실제 application에서, 당신은 많은 coroutine을 실행할 것이다. Structured concurrency는 그들이 없어지거나 누출되지 않는 것을 보장한다. 바깥쪽 범위는 자식 코루틴들이 완료될 때까지 종료될 수 없다. 또한, Structured concurrency는 코드에서 발생하는 어떠한 에러도 적당히 보고되며, 절대 손실되지 않는 것을 보장한다.

함수 추출하고 리팩토링 하기

launch {...} 내부의 코드블럭들을 함수 단위로 분리해보자. 당신이 이 코드에서 "함수 추출"로 리팩토링을 할 때, 당신은 suspend 키워드와 함께 새로운 함수를 만들게 된다. 이것은 너의 첫번째 suspending 함수이다. suspending 함수는 coroutine 내부에서 다른 일반 함수처럼 사용될 수 있지만, suspending 함수의 추가적인 특징은 다른 suspending 함수를 사용할 수 있다는 것이다. (예시에서 delay와 같이) 코루틴의 실행을 중단하기 위해.

fun main() = runBlocking { // this: CoroutineScope
    launch { doWorld() }
    println("Hello")
}// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

전체 코드 : 이곳을 클릭

Scope builder

다른 빌더에 의해 제공된 Coroutine Scope 이외에도, coroutineScope builder를 통해 당신만의 scope를 선언하는 것도 가능하다. 이것은 Coroutine scope를 생성하며, 모든 자식들이 완료될 때 까지 완료되지 않는다.

runBlocking과 coroutineScope builder는 모두 그들 자신과 자식이 완료될 때까지 기다리기 때문에, 아마 비슷하게 보일 것이다. 주된 차이점은 runBlocking 메소드는 대기를 위해 현재 스레드를 Blocking 한다. 반면에 coroutineScope는 단순히 일시정지하여 다른쪽에서 사용하도록 기본 스레드를 release한다. 이러한 차이 때문에, runBlocking은 일반 함수이고, 코루틴 스코프는 suspending 함수이다.

당신은 coroutineScope를 어떠한 suspending 함수에서나 사용할 수 있다. 예를 들면, 당신은 Hello와 World를 동시에 출력하는 함수를 suspend 함수인 doWorld() 내부로 옮길 수 있다.

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

전체 코드 : 이곳을 클릭

이 코드의 출력은 아래와 같다.

Hello
World!

Scope builder와 동시성

coroutinScope builder는 어떠한 suspending 함수 내에서 동시에 여러개가 수행될 수 있다. 수행하기 위해. 두 개의 coroutine을 doWorld suspending 함수에서 동시에 실행해보자.

// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
    doWorld()
    println("Done")
}

// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
    launch {
        delay(2000L)
        println("World 2")
    }
    launch {
        delay(1000L)
        println("World 1")
    }
    println("Hello")
}

전체 코드 : 이곳을 클릭

launch {...} 블록 사이에 있는 두 코드 조각들을 동시에 수행하여, 시작된지 1초 뒤에 World1이 처음으로 출력되고, 시작된지 2초 뒤에 World 2가 출력됐다. doWorld 안에 coroutineScope는 오직 두 작업이 끝난 뒤에 완료되기 때문에, doWorld가 종료된 뒤에 'Done'문자열이 출력될 수 있다.

Hello
World 1
World 2
Done

An explicit job

'launch' Coroutine Builder는 Job 객체를 반환하며, 실행된 Coroutine에 대한 핸들이며 Job 객체가 완료될 때까지 명시적으로 대기하는 데 사용할 수 있다. 예를 들어, 당신은 자식 코루틴이 완료될 때 까지 기다린 뒤에 "Done" 문자열을 출력하게 할 수도 있다.

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") 

전체 코드 : 이곳을 클릭

실행 결과 :

Hello
World!
Done

Coroutines 은 가볍다!

아래 코드를 실행해보자.

import kotlinx.coroutines.*

//sampleStart
fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}
//sampleEnd

전체 코드 : 이곳을 클릭

이것은 10만개의 coroutine을 실행하고, 5초 뒤에 각 coroutine들은 . 을 찍는다.

이제 , 스레드로 실행해보자. (runBlocking을 지우고, launch를 스레드로 바꾸고, delay를 Thread.sleep로 바꾼다.) 어떤 일이 벌어질까? (대부분 당신의 코드는 OOM 오류를 출력할 것이다.)

원문

This section covers basic coroutine concepts.

Your first coroutine

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.

Coroutines can be thought of as light-weight threads, but there is a number of important differences that make their real-life usage very different from threads.

Run the following code to get to your first working coroutine:

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello") // main coroutine continues while a previous one is delayed
}

전체 코드 : 이곳을 클릭
You will see the following result:

Hello
World!

Let's dissect what this code does.

launch is a coroutine builder. It launches a new coroutine concurrently with the rest of the code, which continues to work independently. That's why Hello has been printed first.

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.

runBlocking is also a coroutine builder that bridges the non-coroutine world of a regular fun main() and the code with coroutines inside of runBlocking { ... } curly braces. This is highlighted in an IDE by this: CoroutineScope hint right after the runBlocking opening curly brace.

If you remove or forget runBlocking in this code, you'll get an error on the launch call, since launch is declared only in the CoroutineScope:

Unresolved reference: launch

The name of runBlocking means that the thread that runs it (in this case — the main thread) gets blocked for the duration of the call, until all the coroutines inside runBlocking { ... } complete their execution. You will often see runBlocking used like that at the very top-level of the application and quite rarely inside the real code, as threads are expensive resources and blocking them is inefficient and is often not desired.

Structured concurrency

Coroutines follow a principle of structured concurrency which means that new coroutines can be only launched in a specific CoroutineScope which delimits the lifetime of the coroutine. The above example shows that runBlocking establishes the corresponding scope and that is why the previous example waits until World! is printed after a second's delay and only then exits.

In a real application, you will be launching a lot of coroutines. Structured concurrency ensures that they are not lost and do not leak. An outer scope cannot complete until all its children coroutines complete. Structured concurrency also ensures that any errors in the code are properly reported and are never lost.

Extract function refactoring

Let's extract the block of code inside launch { ... } into a separate function. When you perform "Extract function" refactoring on this code, you get a new function with the suspend modifier. This is your first suspending function. Suspending functions can be used inside coroutines just like regular functions, but their additional feature is that they can, in turn, use other suspending functions (like delay in this example) to suspend execution of a coroutine.

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

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

전체 코드 : 이곳을 클릭

Scope builder

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.

You can use coroutineScope from any suspending function. For example, you can move the concurrent printing of Hello and World into a suspend fun doWorld() function:

fun main() = runBlocking {
    doWorld()
}

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

전체 코드 : 이곳을 클릭

Hello
World!

Scope builder and concurrency

A coroutineScope builder can be used inside any suspending function to perform multiple concurrent operations. Let's launch two concurrent coroutines inside a doWorld suspending function:

// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
    doWorld()
    println("Done")
}

// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
    launch {
        delay(2000L)
        println("World 2")
    }
    launch {
        delay(1000L)
        println("World 1")
    }
    println("Hello")
}

전체 코드 : 이곳을 클릭

Both pieces of code inside launch { ... } blocks execute concurrently, with World 1 printed first, after a second from start, and World 2 printed next, after two seconds from start. A coroutineScope in doWorld completes only after both are complete, so doWorld returns and allows Done string to be printed only after that:

Hello
World 1
World 2
Done

An explicit job
A launch coroutine builder returns a Job object that is a handle to the launched coroutine and can be used to explicitly wait for its completion. For example, you can wait for completion of the child coroutine and then print "Done" string:

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") 

전체 코드 : 이곳을 클릭

실행 결과 :

Hello
World!
Done

Coroutines ARE light-weight

Run the following code:

import kotlinx.coroutines.*

//sampleStart
fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}
//sampleEnd

전체 코드 : 이곳을 클릭

It launches 100K coroutines and, after 5 seconds, each coroutine prints a dot.

Now, try that with threads (remove runBlocking, replace launch with thread, and replace delay with Thread.sleep). What would happen? (Most likely your code will produce some sort of out-of-memory error)

profile
A fast learner.

0개의 댓글