본문 바로가기
개발 지식/Kotlin

[Coroutine] CoroutineBasic

by 에르주 2022. 8. 23.
반응형

인프런에 코루틴을 검색해보니 다음과 같은 강좌가 있어 공부겸해서 블로그에 정리해보려고 한다. 

https://www.youtube.com/c/%EC%83%88%EC%B0%A8%EC%9B%90

 

새차원

새차원의 코틀린 강좌 입니다. http://blog.naver.com/cenodim

www.youtube.com

 

1. 코루틴 (Co+ routine) 으로 루틴의 일종이다. 

 

(안드로이드 앱 개발은 kotlin으로 진행되는데 안드로이드 앱 쓰레드 개념을 보면 UI Thread가 메인 쓰레드이며 이 쓰레드 외에는 UI를 제어 할 수 없다.
UI Thread 외 다른 기능이 동시에 필요하다면 AsynTask를 이용하거나(Deprecated 된 것으로 알고 있음) Sub Thread 또는 Handler를 활용하여 이 결과를 UI Thread에 적용해야 했다.

 

On Android, coroutines help to manage long-running tasks that might otehrwise block the main thread and caouse our app to become unreponsive

 

그렇기 때문에 매번 Thread를 만들어 내는 것은 큰 리소스였고 메인쓰레드에 의해 blocking 되고 앱이 죽어버렸다. 이를 coroutine 리엑트의 개념이 등장 한 것 같다.)

 

 

보통 함수의 실행은 함수 안의 로직을 모두 실행한 뒤 종료하는 프로세스로 진행된다.

fun main() {
   var str = sample()
   
   println(str)
}

fun sample() : String {
    
    return "Hello Erjuer Blog"
}

다음과 같은 함수가 있다고 해보자. 실행 프로세스를 그려보면 

  • main 함수 실행
  • sample 함수 호출 및 실행 
  • sample 함수 종료 
  • main 함수 종료

즉 함수가 한번 실행하면 바로 끝나게 된다.

하지만 코루틴은 이전에 자신의 실행이 마지막으로 중단되었던 지점 다음 장소에서 실행을 재개한다. 즉 함수 안에서 중단/재개를 반복적으로 설정할 수 있다. 

 

이를 코드로 살펴 보기로 한다. 

모두 "Hello, world!" 를 출력하는 기능이다.

 

추가로 실행하는 쓰레드를 표현 할 수 있게 println을 재구성하였다.

 

fun <T>println(msg: T){
    kotlin.io.println("$msg [${Thread.currentThread().name}]")
}

 

1. GlobalScope 이용하기

여기서 GlobalScope는 kotlinx.coroutines.CoroutineScope으로 Lifetime이 프로그램 전체를 가진 scope이다. 즉 프로그램 끝날 때까지 돌아간다.

fun main00() {
    GlobalScope.launch { 
        delay(1000L)
        println("world!")
    }
    println("Hello, ")
    Thread.sleep(2000L)
}

GlobalScope를 제외하고 보자면 main00 함수는 

  1. println("Hello, ") 실행
  2. 2초 대기

이렇게 실행 프로세스가 진행 될 것 이다.

 

main00 함수 내 GlobalScope라는 작은 서브 스레드가 돌아간다고 이해하면 더 쉬울 것 같다. 즉 main00 함수에서 2초 대기 하는 동안 GlobalScope 또한 실행되고 있는 것이다. 그리고 1초뒤에 GlobalScope 내에 println이 실행된다.

 

함수 실행 프로세스는 다음과 같다.

  • println("Hello, ")
  • GlobalScope.launch 1초 쉬고
  • println("world!")
  • Thread.sleep -> 1초 남음 (위에서 1초 쉬었으므로)
  • 종료

Hello, (1초 쉬고) world! (1초 쉬고) 끝.

 

 

2. thread만 활용해보기

그렇다면 굳이 GlobalScope를 이용하는 것이 아닌 sub Thread로 구현할 수 있지 않을까..? 

구현할 수 있다.
fun main01() {

    thread {
        Thread.sleep(1000L)
        println("world!")
    }
    println("Hello,")
    Thread.sleep(2000L)  // sleep은 메인 쓰레드 블로킹 적용

}

결과 값은 같다. 즉 coroutine과 Thread의 기능은 유사하다는 것을 확인 할 수 있다.

 

 

3. runBlocking 이용하기

그렇다면 함수 내 GlobalScope 하나만 존재할 수 있는 것 일까? 아니다

fun main02() {

    GlobalScope.launch {
        delay(1000L) // delay는 suspend 함수 : 일시 중단 함수
        println("world!")
    }

    println("Hello,")
    runBlocking {
        delay(2000L)
    }
}

 

위에서 GlobalScope의 launch과 runBlocking은 코루틴을 만들어 주는 빌더이자 함수이다  차이점은 다음과 같다.

  • launch : 자신을 호출한 쓰레드를 Blocking 하지 않는다.
  • runBlocking : 자신을 호출한 쓰레드를 Blocking 한다.

 

4. 모두 다 같이 runBlocking

GlobalScope.launch 및 runBlocking 모두 코루틴을 만들어주는 빌더 이므로 합칠 수 있다. 

즉 runBlocking 내부에 GlobalScope를 더 만들 수 있다.

fun main03() =
    // 이 함수 내용이 완료되기 전에는 return 하지 않는다 -> 전체를 runBlocking한다.
    runBlocking {
        GlobalScope.launch {
            delay(1000L) // delay는 suspend 함수 : 일시 중단 함수
            println("world!")
        }
        println("Hello,")
        delay(2000L)
    }

 

5. GlobalSope.launch 내의 delay가 값이 runBlocking delay 보다 크다면?

fun main04() =
    // 이 함수 내용이 완료되기 전에는 return 하지 않는다 -> 전체를 runBlocking한다.
    runBlocking {
        GlobalScope.launch {
            delay(3000L) // delay는 suspend 함수 : 일시 중단 함수
            println("world!")
        }
        println("Hello,")
        delay(2000L)
    }

이 함수는 Hello, 출력뒤 2초 뒤면 프로그램이 끝나는데 coroutine은 3초 뒤에 실행되기 때문에 world! 출력이 되지 않는다.

 

 

6. delay 대신 job 사용하기

delay를 사용하게 되면 매번 스코프 마다 실행 시간을 맞춰줘야 한다. 특히 reactive 형태는 언제 데이터가 조회 될지 그리고 얼마나 걸릴 지 모르기 때문에 delay 값을 알맞게 설정할 수 가 없다.

 

이에 coroutine 빌더가 리턴하는 값인 job을 활용하는 방법이 있다.(kotlinx.coroutines.Job)

job 객체로 받아 join 함수를 활용하여 job 객체 수행이 완료 될 때 까지 기다린다.

fun main05() =
    // 이 함수 내용이 완료되기 전에는 return 하지 않는다 -> 전체를 runBlocking한다.
    runBlocking {
        val job = GlobalScope.launch {
            delay(1000L) // delay는 suspend 함수 : 일시 중단 함수
            println("world!")
        }
        println("Hello,")
        job.join() // job이 완료 될때까지 기다린다. (join)
    }

 

 

7. 여러개의 job 객체 생성하기 즉 여러개의 코루틴 생성하기

Structure concurrency (의역... 구조 병행..?) 라고 하기도 하는데 여러개의 코루틴을 생성하여 구조를 관리 및 "병행" 하는 것이다.

밑의 예제는 job2를 하나 더 생성하였으므로 world! 가 두개 찍히게 된다.

fun main06() =
    // 이 함수 내용이 완료되기 전에는 return 하지 않는다 -> 전체를 runBlocking한다.
    runBlocking {
        // coroutine 빌더가 리턴하는 값은 job 객체이다.
        //  kotlinx.coroutines.Job
        val job1 = GlobalScope.launch {
            delay(1000L) // delay는 suspend 함수 : 일시 중단 함수
            println("world!")
        }

        val job2 = GlobalScope.launch {
            delay(1000L) // delay는 suspend 함수 : 일시 중단 함수
            println("world!")
        }
        println("Hello,")
        job1.join() // job이 완료 될때까지 기다린다. (join)
        job2.join()
    }

실행 결과

 

8. GlobalScope 생략 

같은 runBlocking안에 있으므로 GlobalScope를 생략할 수도 있다. 또한 this.launch로도 표현할 수 있는데 this 또한 생략 할 수 있다.

람다 안쪽에서 생성했기 때문이다.

 

부모 코루틴이 차일드 코루틴 만들어 주는 것을 가디란다.

fun main07() =
    // 이 함수 내용이 완료되기 전에는 return 하지 않는다 -> 전체를 runBlocking한다.
    runBlocking {
        // coroutine 빌더가 리턴하는 값은 job 객체이다.
        //  kotlinx.coroutines.Job
        launch {
            delay(1000L) // delay는 suspend 함수 : 일시 중단 함수
            println("world!")
        }

        launch {
            delay(1000L) // delay는 suspend 함수 : 일시 중단 함수
            println("world!")
        }
        println("Hello,")

    }

실행 결과

9. launch -> suspend function Extract

코루틴 생성을 외부 함수로 Extract 할 수도 있다.

myWorld함수가 suspend 함수 이기 때문에 delay를 사용할 수 있다.

// suspend function Extract
fun main08() = runBlocking {
    launch {
        myWorld()
    }
    println("Hello,")
}

suspend fun myWorld() {
    // suspend 함수가 아닐 수 Suspend function 'delay' should be called only from a coroutine or another suspend function
    delay(1000L)
    println("world,")
}

 

10. Thread vs coroutine

위에서 Thread와 coroutine의 동작 원리는 유사하다 했는데 그렇다면 왜 coroutine을 쓰라고 하는 것일까?

다음과 같은 코드로 코루틴이 쓰레드보다 더 가볍게 동작한 다는 것을 확인 할 수 있다.

// 코루틴은 가볍다! -> 증명
fun main09() = runBlocking {

    repeat(100_000) {
        launch {
            delay(1000L)
            print(".")
        }
    }
}

// 쓰레드는 무겁다! -> 증명
fun main10() = runBlocking {

    repeat(100_000) {
        thread {
            Thread.sleep(1000L)
            print(".")
        }
    }
}

 

11. 단. 코루틴보다 메인 쓰레드가 우선이다.

main11 함수에서 delay가 1.3초이고 GlobalScope.launch로 생성된 코루틴이 0.5초 간격으로 1000번 실행된다고 생각해보자.

 

여기서 GlobalScope 코루틴이 살아 있다고 해도 부모 코루틴(runBlocking)이 1.3초만에 프로세스가 종료하게 되므로 1000번을 모두 실행하지 못한다.

fun main11() = runBlocking {
    GlobalScope.launch {
        repeat(1000) { i->
            println("I'm sleeping $i ... ")
            delay(500L)
        }
    }
    delay(1300L) // 메인함수는 1.3초 뒤에 끝남
}

더 실행이 안됌!

 

12. 일시 중단 재개

다음과 같은 함수의 결과는  Coroutine A, Coroutine B이 차례대로 실행된다.

fun main12() = runBlocking {
    launch {
        repeat(5) { i->
            println("Coroutine A, $i")
        }

    }
    launch {
        repeat(5) { i->
            println("Coroutine B, $i")
        }
    }
    println("Coroutine Outer")
}

실행결과

 

중간에 Delay로 중단을 걸어주면 Coroutine A, Coroutine B 두개의 코루틴이 번갈아 가며 일시 중단 그리고 재개 되는 것을 확인 할 수 있다.

fun main13() = runBlocking {
    launch {
        repeat(5) { i->
            println("Coroutine A, $i")
            delay(10L) // 코루틴 중단 A -> B
        }

    }
    launch {
        repeat(5) { i->
            println("Coroutine B, $i")
            delay(10L) // 코루틴 중단 B -> A
        }
    }
    println("Coroutine Outer")
}

 

끝.

반응형

댓글