[Android] 코루틴 기초

    저번 코루틴은 왜 써야하는가 에 이어서 이번에는 코루틴의 기초에 대해 공부해 보겠습니다!

     

     

    [ First Coroutine ]

      
      fun main() {
          GlobalScope.launch { // 백그라운드에서 코루틴을 생성하고 진행합니다 
              delay(1000L) // 1초 동안 지연
              println("World!") // 지연 후 출력
          }
          println("Hello,") // 코루틴이 지연되는 동안 메인쓰레드는 계속 됩니다
          Thread.sleep(2000L) // JVM을 유지하기 위해 2초동안 메인 스레드를 차단합니다
      }
      

    코루틴 문서에서는 이것을 쓰레드로 시도해 볼것을 권유했습니다.

      fun Main() {
      	thread {
       	  //delay(1000L)
    	  Thread.sleep(2000L)
    	  println("world")
    	{
        
    	println("Hello,")
    	Thread.sleep(2000L)
      }
      

    그래서 변경 후 실행을 해보니 코루틴을 사용한것과 쓰레드를 사용했을때 같은 결과가 나왔습니다.

     

    • Coroutine는 light-weight threads 이다.
    • GlobalScope 는 전역 스코프이다.
    • launch는 Coroutine Builder 이며 내부적으로 코루틴을 만들어서 반환해 준다.
    • Builder를 사용하기 위해서는 Scope가 필요하다.

     

     

     

    [ Bridging blocking and non-blocking worlds ]

      fun main() { 
          GlobalScope.launch { // 백그라운드에서 새 코루틴을 시작
              delay(1000L)
              println("World!")
          }
          println("Hello,") // 메인 스레드가 여기서 즉시 시작됩니다
          runBlocking {     // 런블록킹이 기본 스레드를 차단합니다
              delay(2000L)  // JVM을 유지하기 위해 2초동안 지연시킵니다
          } 
      }
      

    이전 예제에서는 delay (suspend function으로 일시 중단되는 함수) 형태의 지연처리와 sleep (threadblocking 하는 함수) 형태의 지연처리를 혼용해서 사용했기 때문에 일시 중단되는 함수로 맞춰보겠습니다.

    일시 중단되는 함수로 맞추기 위해서는 Thread.sleep 부분을 delay 로 변경해주면 되는데 그냥 변경을 하면 사용할 수가 없습니다.

    그 이유는 delaysunpend function이기 때문에 코루틴의 스코프 안에서만 사용을 할수 있기 때문입니다

    그래서 명시적으로 blocking을 하며 코루틴을 만들어줄수 있는 runBlocking를 사용할 수 있습니다

     

     

      fun main() = runBlocking { // 메인 코루틴 시작
         GlobalScope.launch { // 백그라운드에서 새 코루틴을 시작
             delay(1000L)
             println("World!")
         }
         println("Hello,") // 메인 코루틴이 계속됩니다
         delay(2000L)  // JVM을 유지하기 위해 2초동안 지연시킵니다
      }
      

    위의 예제를 좀더 관용적으로 변경한 것 입니다.

    위의 예제는 메인함수가 함수 안의 내용이 완료되기 전까지는 리턴되지 않기를 바라기 때문에 runBlocking 을 마지막에만 사용하는 것이 아니라 코드 전체를 감싸줘서 관용적인 코드로 만들어 줍니다.

     

     

    • runBlocking 도 코루틴 빌더이다
    • launch는 자신을 호출한 쓰레드를 blocking 하지 않지만, runBlocking는 blocking 한다
    • runBlocking 는 내부 코루틴이 완료 될때까지 메인 스레드를 호출한다

     

     

     

    [ Waiting for a job ]

      fun main() = runBlocking { // 메인 코루틴 시작
         GlobalScope.launch { // 백그라운드에서 새 코루틴을 시작
             delay(3000L) //JVM은 2초간만 유지하기때문에 실행되지 못하고 종료됨
             println("World!")
         }
         println("Hello,") // 메인 코루틴이 계속됩니다
         delay(2000L)  // JVM을 유지하기 위해 2초동안 지연시킵니다
      }
      

    위의 예제에서는 delay를 이용하여 작업이 끝나기를 기다렸는데 그렇게 되면 메인 코루틴이 서브 코루틴보다 일찍 끝나는 시점에서 원하는 결과값을 받을 수 없게 됩니다.

     

    위의 예제처럼 delay를 사용하는 방법은 좋은 방법은 아닙니다

    이러한 문제점을 해결하기 위해 delay를 사용하지 않고도 작업을 기다릴수 있는 job을 이용해 보겠습니다.

     

      
      fun main() = runBlocking {
         val job = GlobalScope.launch { // 새 코루틴을 시작하고 작업에 대한 참조를 유지합니다.
           delay(1000L)
           println("World!")
         }
         println("Hello,")
         job.join() // 자식 코루틴이 완료 될때까지 기다립니다
      }
      

     

    • launch가 반환하는 것은 job 이다
    • job에다가 join을 하게되면 해당 코루틴이 완료 될때까지 기다렸다가 main함수를 종료합니다.

     

     

     

     

    [ Structured concurrency ]

      
      fun main() = runBlocking {
         val job = GlobalScope.launch { // 새 코루틴을 시작하고 작업에 대한 참조를 유지합니다.
           delay(1000L)
           println("World!")
         }
         println("Hello,")
         job.join() // 자식 코루틴이 완료 될때까지 기다립니다
      }
      

    이전 예제에서 코루틴을 완료되는걸 기다리기 위해 sleep를 주기도 하고 job을 사용하기도 했습니다.

    job이 가장 나은 방식이었지만 여러 코루틴을 실행하게 됐을때 job이 늘어나 관리하기도 어려워 지게됩니다.

     

    위의 예제는 runBlocking 내에 새로운 GlobalScope를 생성한 상태입니다.

    부모의 코루틴과 job 객체의 코루틴이 구조적으로 아무런 관련이 없습니다.

    job의 join을 이용하지않는다면 job의 완료여부와 상관없이 runBlocking 이 종료되게 됩니다.

     

    이런것들을 방지하기 위해 더 나은 솔루션인 Structured concurrency(병행성) 가 있습니다.

     

     

      fun main() = runBlocking { // CoroutineScope
          this.launch { // runBlocking 의 범위에서 새 코루틴을 시작합니다 // this는 생략 가능
              delay(1000L)
              println("World!")
          }
          println("Hello,")
      }
      

    위의 job을 이용한것과 동일한 결과가 나옵니다.
    새로운 스코프를 생성하는 것이 아닌 부모의 코루틴 스코프를 받아서 처리하게 되면 순서를 보장하게 되고 연속된 작업이 가능합니다.
    부모의 코루틴을 받아서 처리했기 때문에 this.launch 의 작업이 끝나기 전까지 부모 코루틴은 종료되지 않습니다

     

     

     

     

    [ Suspend Function ]

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

    launch 스코프 내의 코드를 별도의 함수로 추출해본다고 했을때 suspend를 붙이지 않는다면 에러가 뜨게됩니다.

    suspend를 붙여주면 delay와 같이 코루틴 내부에서 사용할수 있는 기능들을 사용할수 있게 됩니다

     

     

     

    [ Coroutines ARE light-weight ]

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

    코루틴은 정말 가볍습니다.

    위의 예제를 launch를 사용하지 않고 thread를 이용해 sleep을 걸어서 실행해본다면 확연한 성능 차이를 느낄 수 있습니다.

     

     

     

    [ Global coroutines are like daemon threads ] 

      
      fun main() = runBlocking{
        GlobalScope.launch {
            repeat(1000) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
        }
        delay(1300L) // 지연 후 종료
      }
      

    코루틴이 계속 실행 되고 있다고해서 프로세스가 유지 되는건 아닙니다.

    데몬쓰레드 처럼 프로세스가 살아있을때만 동작을 할 수 있습니다.

     

    위의 예제는 Globalscope에서 launch가 되는데 0.5초 마다 1000번을 반복 동작하는 코드입니다.

    위의 결과를 보면 0,1,2 까지만 반복되고 종료되는데 이 결과를 보면 코루틴이 살아있다고 해서 프로세스를 계속 유지시키는건 아니라는걸 알 수 있습니다.

     

     

     

     

    [ suspend <-> resume 체험 ] 

      fun main() = runBlocking{
    
          launch{
          	  repeat(5){ i ->
              	  println("Coroutine A, A Thread: ${Thread.currentThread()}")
                  delay(10L)
              }
          }
    
          launch{
        	  repeat(5){ i ->
            	  println("Coroutine B, B Thread: ${Thread.currentThread()}")
                  delay(10L)
              }
          }
        
          println("parent Coroutine Finished) 
      }
      

    일시 중단과 재개에 대한 내용을 알아 볼수 있는 코드입니다.

    처음 delay를 주지않고 실행을 하게 되면 순차적으로 호출이 됩니다.

    parent Coroutine Finished -> Coroutine A 0, 1, 2, 3, 4 -> Coroutine B 0, 1, 2, 3, 4

     

    delay를 10L 씩 주게되면

    parent Coroutine Finished -> Coroutine A 0 -> Coroutine B 0 -> Coroutine A 1 -> Coroutine B 1 ......

    이렇게 일시중단과 재개가 이루어 지는걸 볼 수 있게 됩니다.

     

     

    코루틴 기초에 대해서 알아보았습니다!

     

     

     

     

     

     

     

     

     

     

     

    references :

     

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

    www.youtube.com/channel/UCJeARDV434voq3IxRTBfzLw

    댓글