Kotlin Coroutine 정리(1, Basics)

이 포스팅에 대한 업데이트 버전을 아래 링크와 같이 게시하였음

https://witcheryoon.tistory.com/291?category=893157

 

 

해당 포스팅은 kotlinlang.org/docs/reference/의 가이드에서 Coroutines 부분을 요약 정리한 것과 더불어 다른 여러 사이트들을 참고할 것임.

 

참고 사이트 : medium.com/@limgyumin/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%A0%9C%EC%96%B4-5132380dad7f.

 

medium.com/harrythegreat/%EB%B2%88%EC%97%AD-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4%EC%9D%98-%ED%8C%A8%ED%84%B4%EA%B3%BC-%EC%95%88%ED%8B%B0%ED%8C%A8%ED%84%B4-6e97f852ea2d

 

12bme.tistory.com/582

 

1. kotlinlang.org/docs/reference/coroutines/coroutines-guide.html

 

일단, 코루틴에서 기억할만한 핵심 키워드는 다음과 같다.

 

async

await

launch

aysnc

 

 

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

 

- Your first coroutine

(위의 예제를 참고해서)

 

1>코루틴의 핵심(Essential)은 weight가 작은 쓰레드라는 것이다. 즉 코루틴은 "경량 쓰레드"이다.

2> [launch] 를 사용하여 어떤 CoroutineScope의 context 안에서 coroutine builder를 동작(launch)한다.

3> 이때 launch는 새로운 코루틴을 GlobalScope안에서 launching 하며.. 이 의미는, 새로운 코루틴의 lifetime 한계가 어플리케이션의 lifetime에 limited된다는 것이다.

4> 위 코드에서 global scope 안의 코루틴은 non blocking이고, Thread.sleep은 blocking 방식임을 참고하라.

 

기억할 키워드

CoroutineScope : CoroutineScope 는 말 그대로 코루틴의 범위, 코루틴 블록을 묶음으로 제어할수 있는 단위

GlobalScope : GlobalScope 는 CoroutineScope 의 한 종류이며,  미리 정의된 방식으로 프로그램 전반에 걸쳐 백그라운드 에서 동작

launch

 

-Bridging blocking and non-blocking worlds

위의 첫번째 예제는 blocking과 non blocking이 공존한다. 이 방법은 자칫하면 어떤 것을 어느 상황에 써야할지에 대하여 혼선을 가져다 줄 수 있으므로, runBlocking coroutine builder을 사용하여 명시적으로 blocking을 구현 할 수 있다.

 

아래는 그에 대한 예제이다.

 

 

1> 1번째 예제와 결과는 동일하다. 허나 이 코드는 non-blocking 딜레이만 사용한다.

2> runBlocking이 complete 될때까지 main thread는  runBlokcing block 을 활성화(invoke)한다.

 

1> 이 예제는 runBlocking으로 메인 function의 execution을 감싼 예이다.

2> 이때 runBlocking<Unit>은 adaptor로써 동작하고, top level의 메인 코루틴을 시작시키는데 사용할 수 있다

3> 명시적으로 Unit return 타입을 구체화하는데, 그 이유는 코틀린에서 잘 정의된 main function은 Unit을 리턴 시키기 때문이다.

 

 

------------------------

기억할 키워드

CoroutineScope : CoroutineScope 는 말 그대로 코루틴의 범위, 코루틴 블록을 묶음으로 제어할수 있는 단위

GlobalScope : GlobalScope 는 CoroutineScope 의 한 종류이며,  미리 정의된 방식으로 프로그램 전반에 걸쳐 백그라운드 에서 동작

launch

runBlocking

------------------------

-Waiting for a job

1> 다른 코루틴(B)이 동작하고 있을 동안, 코루틴(A)이 딜레이 되는 상황은 좋은 접근이 아니다.

2> (따라서) Launched된 백그라운드 Job이 complete 될 동안,  non blocking 방식으로 명시적인 wait를 아래와 같이 만들수 있다

3> 이 예제에서의 결과는 이전과 동일하지만, 어떠한 방식으로든 메인 코루틴이 백그라운드 Job에 종속되어있지는 않으므로 이전 방식보다 더 낫다고 할 수 있다

 

 

주요 키워드 설명

----------------------------------------------------------------------

기억할 키워드

CoroutineScope(CoroutineScope) : CoroutineScope 는 말 그대로 코루틴의 범위, 코루틴 블록을 묶음으로 제어할수 있는 단위

GlobalScope(GlobalScope) : GlobalScope 는 CoroutineScope 의 한 종류이며,  미리 정의된 방식으로 프로그램 전반에 걸쳐 백그라운드 에서 동작

launch(launch)

runBlocking(runBlocking) : 내부 작업이 종료될때가지 일시 중지된다.

job(Job) : launch() 함수로 호출한 코루틴 블록이 가지는 객체이름을 의미

join(join): Join() 함수로 코루틴 블록이 완료될 때까지 다음 코드 수행을 대기 할 수 있다.

joinAll() : 모든 launch 코루틴 블록이 완료 되기를 기다릴수도 있다.

 

---------------------------------------------------------------------

-Structured concurrency

1> 코루틴의 실질적인 사용을 위한 바람직한 방법들이 존재한다.

2> GlobalScope.launch를 사용하면, top-level coroutine을 만드는 행위가 된다. 코루틴이 경량 쓰레드라할 지라도, 동작을 하게 된다면, 메모리를 소비할 것이고.. 새롭게 만들어지는 코루틴을 참조하는 것을 잊었더라도 코루틴은 계속 동작한다. 그렇다면.. 딜레이가 너무 되서 멈추는 경우나.. 코루틴 launch를 너무많이해서 oom(out of memroy)상황이 되는 상황이 올 것인데.. 이들을 모두 참조로 해서 join으로 관리한다는 것은 에러가 발생하기 쉬운 상황을 만들어낼 수 있다

 

3>위의 예를 해결하는 더 좋은 해법이 존재한다. 코드 안에 구조화된 동시성(structured concurrency)를 사용하면 된다. 이는 GlobalScope안에 코루틴을 launching 하는 대신에(사실 이방법은 threads를 사용하는 방법과 비슷한데, thread는 항상 global이기 때문) 수행하는 작업의 동작의 specific한 scope안에 coroutine을 launch할 수 있다.

 

 

4> 이 예제는 main에서, 코루틴빌더인 runBlocking을 사용하여 코루틴으로 변환해주는 함수가 있다.

5> runBlocking을 포함한 모든 코루틴 빌더는 각 코드 블록의 범위에 CoroutineScope를 붙여준다(add). 이렇게한다면, 이 범위내에서(in this scope) 명시적으로 join을 하지 않고서도 코루틴을 launch할 수 있는데, 외부 코루틴(이 예제에서는 runBlocking)은 모든 코루틴들이 각자의 scope내에서 complete 되기 전까지 complete되기 않기 때문이다. 그렇기에 위의 구조화된 동시성을 사용한다면 코드를 더 간단히 작성할 수 있다.

 

-Scope builder

 

1> 다양한 빌더에 의해서 coroutine scope가 구성되는 것(provided)에 덧붙여서 설명하자면, 사용자 자신만의 scope를 coroutineScope builder를 선언하여 구성할 수 있다. 코루틴 scope를 만들고 그 코루틴안에서 launched된 children들이 complete 되기전까지, 사용자 자신이 구성한 그 코루틴은 complete되지 않는다.

 

2> runBlocking과 coroutineScope는 그것들의 아이들(children)이 complete될때까지 body 자체가 complete 되기전까지 wait한다는 유사성이 있다. 이 둘의 주요 차이는 runBlocking 메소드는 현재 스레드가 waiting을 할 때까지 기다린다(block)는 것이고, coroutineScope는 다른 용도의 사용을 위해 기본 스레드를 해제(release)하고 일시중지(suspend)한다. 이 차이 때문에, runBlocking은 regular function이고, coroutineScope는 suspendsing function과 같다(is).

(coroutineScope가 아직 완료되지 않은 경우에도 "Task from coroutine scope"메시지 (중첩 된 시작을 기다리는 동안) 바로 뒤에 "Task from runBlocking"이 실행되고 표시된다)

 

아래는 위의 예제를 응용해서 좀더 복잡하게 구성해본것이다

 

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking launch1(mainthread Operating)")
    }
    
    coroutineScope { // Creates a coroutine scope
        println("New-A coroutine scope start(New-A)")
        launch {
            println("Task from nested New-A launch start(New-A)")
            delay(500L) 
            println("Task from nested New-A launch 500 delayed(block end (New-A))")
        }
        delay(100L)
        println("New-A coroutine scope end 100(New-A)")
    }
    
    launch { 
        delay(1000L)
        println("Task from runBlocking launch2(mainthread Operating)")
    }

    launch { 
        delay(1200L)
        println("Task from runBlocking launch3(mainthread Operating)")
    }

    
    coroutineScope { // Creates a coroutine scope
    println("New-B coroutine scope start(New-B)")
        launch {
            delay(500L) 
            println("Task from nested New-B launch(New-B)")
        }
    
        delay(100L)
        println("New-B coroutine scope end(New-B)") 
    }
        
    println("Line End msg(mainthread Operating)")
}

 

위 예제를 실행했을때의 결과이고... 생각한 개념이 맞는지를 확인해보자

 

 

- Extract function refactoring

1> launch{ ...}  내부에 코드 블록을 따로 떼어내어 separate function을 만들수 있다. 이때 새롭게 만드는 함수 앞에 suspend 수정자를 앞에 붙여서 이 기능을 수행할 수 있다.

2> suspending function은 regular function과 마찬가지로 다른 코루틴 안에서 사용할 수 있고, regular function에 추가되는 다른 특징으로는, 코루틴의 suspend 실행을 위해 다른 suspending function(delay 같은 함수)을 사용할 수 있다.

 

3> 만약 extracted function(코드를 블록을 따로 떼어낸 suspending function)이 현재 범위(current scope)에서 실행되는(invoked) 코루틴 빌더를 포함한다면, suspend 키워드(수정자, modifier)만으로 충분하지않다. 위의 예제에서 doWorld 메소드를 CoroutineScope에다가 두는것이 하나의 방법이 되겠으나 이 방법은 항상 API을 명확하게 구성해서 applicable하게 하지는 못한다. 관용적 해법은 target함수를 포함하는 클래스의 필드로써 명시적인 CoroutineScope를 사용하거나,  암시적인 CoroutineScope를 사용(외부 클래스에 CoroutineScope를 상속하는 방법으로) 하면 된다 .

 

4> 마지막 방법으로는 CoroutineScope(coroutineContext)를 사용할 수 있다. 그러나 그러한 접근은 구조적으로 불안정한데.. 이 메소드의 실행 범위를 사용자가 더이상 컨트롤할 수 없기 때문이다. 따라서 only private API가 이러한 빌더를 사용한다.

 

- Coroutines ARE light-weight

1> 위 코드는 코루틴이 light weight임을 보여주는 코드이다. 저 코드를 쓰레드로 구현한다고 생각해본다면.. oom과 관련한 에러가 나게 될 것이다. 하지만 코루틴은 경량이기 때문에 저렇게 구성해도 메모리 관련 오류가 나지않는다.

 

- Global coroutines are like daemon threads

 

위 코드는 장기간 동작하는 코루틴을 glboalscope에 launch한다. 나는 잠온다는 말을 0.5초 주기로 프린팅하고, 메인 함수로 리턴한다.

GlobalScope에서 시작된 활성 코루틴은 프로세스를 활성 상태로 유지하지 않는다. 이는 데몬 스레드와 같은 방식으로 작동한다. (데몬(daemon) 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 하는 스레드이며,

주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료된다)

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유