[Kotlin] Ch6-2. 코루틴 기본

leeeha·2022년 9월 8일
0

코틀린

목록 보기
28/29
post-thumbnail

출처: https://www.boostcourse.org/mo234/lecture/154329?isDesc=false

코루틴 (coroutine)

  • 스레드와 달리 코틀린은 코루틴을 통해 코드의 복잡도를 줄일 수 있고, 손쉽게 일시 중단하거나 다시 시작하는 루틴을 만들어낼 수 있다.
  • 멀티태스킹을 실현하면서 가벼운 스레드라고도 불린다. (스레드는 자신의 스택을 별도로 갖고 있지만, 코루틴은 Stackless로 스택을 갖지 않아서 생성 오버헤드가 줄어든다는 차이점이 있다.)
  • 하나의 스레드에 다수의 코루틴이 생성될 수 있으므로 코루틴은 스레드보다 좀 더 작은 단위이다.
  • 코루틴은 문맥 교환 없이 해당 루틴을 일시 중단 (suspend)을 통해 제어한다.

코루틴의 주요 패키지

kotlinx.coroutines의 common 패키지

기능설명
launch, async코루틴 빌더
Job, Deferredcancellation 지원을 위한 기능 (Job은 생명주기를 갖고 동작하는 작업의 단위)
Dispatchers일종의 스케줄러, Default는 백그라운드 코루틴을 위해, Main은 Android, Swing, JavaFX를 위해 사용됨.
delay, yield상위 레벨 지연 (suspending) 함수
Channel, Mutex통신과 동기화를 위한 기능
coroutineScope, supervisorScope범위 빌더
select표현식 지원

core 패키지

기능설명
CommonPool코루틴 컨텍스트
produce, actor코루틴 빌더

코루틴 빌더

launch

  • 일단 실행하고 잊어버리는 (fire-and-forget) 형태의 코루틴으로 메인 프로그램과 독립적으로 실행될 수 있다.
  • 기본적으로 즉시 실행되며 블록 내의 실행 결과는 반환하지 않는다.
  • 상위 코드를 블록시키지 않고 (넌블로킹) Job 객체를 즉시 반환한다.
  • join()을 통해 상위 코드가 종료되지 않고 코루틴이 완료될 때까지 기다리게 할 수 있다.

async

  • 비동기 호출을 위해 만든 코루틴으로 결과나 예외를 반환한다.
  • 실행 결과는 Deferred<T>를 통해 반환하며 await을 통해 받을 수 있다.
  • await는 작업이 완료될 때까지 기다리게 만든다.

예제

📌 코루틴 라이브러리 추가하기
File > Project Structures > Libraries > From Maven > org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:version 추가
cf) https://stackoverflow.com/questions/52522282/using-kotlinx-coroutines-in-intellij-idea-project

package chap06.section2

import kotlinx.coroutines.*

fun main() { // 메인 스레드의 컨텍스트
    GlobalScope.launch { // 새로운 코루틴을 백그라운드에서 실행
        delay(1000L) // 1초 넌블로킹 지연
        print("World!") // 지연 후 출력
    }
    print("Hello, ") // 코루틴이 지연되는 동안 메인 스레드는 계속 실행
    Thread.sleep(2000L) // 메인 스레드가 JVM에 의해 바로 종료되지 않도록 2초 지연
} 

Hello, World! (Hello, 가 출력되고 1초 뒤에 World! 가 출력됨.)

  • 코루틴을 생성하면, 새로운 루틴이 하나 더 생성된다고 보면 된다.
  • delay()와 같은 suspend 함수는 반드시 코루틴 안에서 사용해야 한다.
  • suspend 함수 중 하나인 delay()는 일시 중단될 수 있으며 필요한 경우 재개된다. 일시 중단되었을 때, 다른 스레드는 블로킹 되지 않고 계속 실행된다. (넌블로킹)
  • 반면에, sleep() 함수는 메인 스레드를 블록킹 시킨다. 위의 코드에서 GlobalScope로 지정된 launch 코루틴의 수행 완료를 기다리지 않으면, 프로그램이 종료되어 코루틴도 같이 사라지게 된다. (코루틴은 프로그램과 생명주기를 같이 함.) 따라서, sleep() 함수로 메인 스레드를 블로킹 시켜야 delay() 함수의 그 다음 부분도 실행될 수 있다.

suspend 함수

public suspend fun delay(timeMills: kotlin.Long): kotlin.Unit { /* compiled code */ }

package chap06.section2

import kotlinx.coroutines.*

fun main() { // 메인 스레드의 컨텍스트 
    //doSomething() // error 
    
    GlobalScope.launch { // 새로운 코루틴을 백그라운드에서 실행
        delay(1000L) // 1초 넌블로킹 지연
        print("World!") // 지연 후 출력

        doSomething() // ok 
    }
    print("Hello, ") // 코루틴이 지연되는 동안 메인 스레드는 계속 실행
    Thread.sleep(2000L) // 메인 스레드가 JVM에 의해 바로 종료되지 않도록 2초 지연
}

suspend fun doSomething() {
    println("Do something!")
}
  • suspend 함수를 코루틴 블록이 아닌 곳에 사용하면 에러가 발생한다.
  • 컴파일러는 suspend 키워드가 붙은 함수를 자동적으로 추출하여, Continuation 클래스로부터 분리된 루틴을 만든다.
  • 코루틴 빌더인 launch와 async 내에서 suspend 함수를 사용할 수 있다.

Job 객체

package chap06.section2

import kotlinx.coroutines.*

fun main() {
    val job = GlobalScope.launch {
        delay(1000L)
        println("World")
        doSomething()
    }
    println("Hello")
    println("job: $job")
    Thread.sleep(2000L)
    println("job: $job")
}

suspend fun doSomething() {
    println("Do something")
}

Hello
job: StandaloneCoroutine{Active}@5b1d2887
World
Do something
job: StandaloneCoroutine{Completed}@5b1d2887

package chap06.section2

import kotlinx.coroutines.*

fun main() {
    val job = GlobalScope.launch {
        delay(1000L)
        println("World")
        doSomething()
    }
    println("Hello")
    println("job: ${job.isActive}, ${job.isCompleted}")
    Thread.sleep(2000L)
    println("job: ${job.isActive}, ${job.isCompleted}")
}

suspend fun doSomething() {
    println("Do something")
}

Hello
job: true, false
World
Do something
job: false, true

  • Job 객체는 코루틴의 생명주기를 관리하며, 생성된 코루틴 작업들은 부모-자식 같은 관계를 가질 수 있다.
  • 부모가 취소(cancel)되거나 실행에 실패하면, 그 하위 자식들도 모두 취소된다.
  • 반대로, 자식이 실행에 실패할 때도 그 부모와 다른 자식들 모두 취소된다.
  • 단, SupervisorJob의 경우 자식이 실행에 실패하더라도 그 부모나 다른 자식에 영향을 주지 않아서 실행을 유지할 수 있다.

join()

  • Job 객체의 join() 함수를 통해 코루틴의 작업이 완료될 때까지 기다리게 할 수 있다.
  • launch에서 반환값을 받으면 Job 객체가 되기 때문에 이를 이용하여 메인 메서드에서 join()을 호출할 수 있다.
  • join() 역시 suspend 함수이므로 코루틴 블록 내에서 실행시켜줘야 한다. (아래 코드에서 runBlocking으로 감싼 이유)
package chap06.section2

import kotlinx.coroutines.*

fun main() {
    runBlocking { 
        val job = GlobalScope.launch { 
            delay(1000L)
            println("World")
            doSomething()
        }
        println("Hello")
        println("job: ${job.isActive}, ${job.isCompleted}")
        //Thread.sleep(2000L)
        job.join()
        println("job: ${job.isActive}, ${job.isCompleted}")
    }
}

suspend fun doSomething() {
    println("Do something")
}

Hello
job: true, false
World
Do something
job: false, true

package chap06.section2

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        val job = GlobalScope.launch {
            delay(1000L)
            println("World")
            doSomething()
        }
        println("Hello")
        println("job: ${job.isActive}, ${job.isCompleted}")
        //Thread.sleep(2000L)
        //job.join()
        println("job: ${job.isActive}, ${job.isCompleted}")
    }
}

suspend fun doSomething() {
    println("Do something")
}

Hello
job: true, false
job: true, false

join()을 해주지 않으면 코루틴의 작업이 완료될 때까지 기다리지 않고, 메인 스레드가 계속 실행되기 때문에 위와 같은 결과가 나오고 프로그램이 종료된다.

Job의 상태


코루틴의 중단과 취소

중단 (코루틴 코드 내에서)

  • delay(ms): 일정 시간을 지연시키며 중단 (넌블로킹) ↔ Thread.sleep()은 블로킹
  • yield(): 특정 값을 산출하기 위해 중단

취소 (코루틴 외부에서)

  • Job.cancel(): 지정된 코루틴 작업을 즉시 취소
  • Job.cancelAndJoin(): 지정된 코루틴이 완료될 때까지 기다렸다가 취소
  • 기본적으로 부모-자식 관계에 적용될 수 있으며, 부모 블록이 취소되면 모든 자식 코루틴이 취소된다.

동기 vs 비동기

package chap06.section2

import kotlinx.coroutines.*

suspend fun doWork1(): String {
    delay(1000)
    return "Work1"
}

suspend fun doWork2(): String {
    delay(3000)
    return "Work2"
}

private fun workInSerial(){
    GlobalScope.launch {
        // 코루틴 내의 suspend 함수는 순차적으로 실행됨.
        val one = doWork1()
        val two = doWork2()
        // 1초, 3초 지나고나서 출력
        println("Kotlin one: $one")
        println("Kotlin two: $two")
    }
}

fun main() {
    workInSerial()
    readLine() // 메인 스레드가 먼저 종료되는 것을 막기 위해 콘솔에서 입력 대기
}
package chap06.section2

import kotlinx.coroutines.*

suspend fun doWork1(): String {
    delay(1000)
    return "Work1"
}

suspend fun doWork2(): String {
    delay(3000)
    return "Work2"
}

private fun workInSerial(): Job {
    val job = GlobalScope.launch {
        // 코루틴 내의 suspend 함수는 순차적으로 실행됨.
        val one = doWork1()
        val two = doWork2()
        // 1초, 3초 지나고나서 출력
        println("Kotlin one: $one")
        println("Kotlin two: $two")
    }
    return job
}

fun main() {
    runBlocking {
        val job = workInSerial()
        job.join()
    }
    //readLine()
}

Kotlin one: Work1
Kotlin two: Work2

package chap06.section2

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun doWork1(): String {
    delay(1000)
    return "Work1"
}

suspend fun doWork2(): String {
    delay(3000)
    return "Work2"
}

private fun workInSerial(): Job {
    val job = GlobalScope.launch {
        // 코루틴 내의 suspend 함수는 순차적으로 실행됨.
        val one = doWork1()
        val two = doWork2()
        // 1초, 3초 지나고 나서 출력
        println("Kotlin one: $one")
        println("Kotlin two: $two")
    }
    return job
}

fun main() {
    runBlocking {
        val time = measureTimeMillis {
            val job = workInSerial()
            job.join()
        }
        println("time: $time")
    }
}

Kotlin one: Work1
Kotlin two: Work2
time: 4077

실행 시간을 보면, doWork1()과 doWork2()가 순차적으로 실행되었다는 걸 알 수 있다. 그렇다면, 특정 suspend 함수들을 비동기적으로 동시에 실행할 수는 없을까?

async 코루틴 빌더

  • 동시성 처리를 위한 코루틴 빌더
  • launch와 다른 점은 Deferred<T>를 통해 실행 결과를 반환한다는 것
  • 지연된 결과 값을 받기 위해 await()를 사용
package chap06.section2

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun doWork1(): String {
    delay(1000)
    return "Work1"
}

suspend fun doWork2(): String {
    delay(3000)
    return "Work2"
}

private fun workInSerial(): Job {
    val job = GlobalScope.launch {
        // 코루틴 내의 suspend 함수는 순차적으로 실행됨.
        val one = doWork1()
        val two = doWork2()
        // 1초, 3초 지나고 나서 출력
        println("Kotlin one: $one")
        println("Kotlin two: $two")
    }
    return job
}

private fun workInParallel(): Job {
    val one = GlobalScope.async {
        doWork1()
    }
    val two = GlobalScope.async {
        doWork2()
    }
    val job = GlobalScope.launch {
    	// one, two가 동시에 실행됨. 
        val combined = one.await() + "_" + two.await()
        println("Kotlin Combined: $combined") 
    }
    return job
}

fun main() {
    runBlocking {
        val time = measureTimeMillis {
            val job = workInParallel()
            job.join()
        }
        println("time: $time")
    }
}

Kotlin Combined: Work1_Work2
time: 3080

doWork1()과 doWork2()가 비동기적으로 동시에 실행되어서 실행 시간이 3초 정도로 나온 것을 볼 수 있다.

profile
습관이 될 때까지 📝

0개의 댓글