본문 바로가기
Kotlin/Kotlin 프로그래밍

[Kotlin/Coroutine] 코루틴(Coroutine)의 개념과 사용

by 주 녕 2021. 6. 16.
반응형

모든 내용은 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다. 

 

프로세스나 스레드는 해당 작업을 중단(stopped)하고 다른 루틴을 실행하기 위한 문맥 교환을 시도할 때 많은 비용이 듦.

코루틴(Coroutine)은 비용이 많이 드는 문맥 교환없이 루틴을 일시 중단(suspended)하여 비용을 줄일 수 있음. 

→ 운영체제가 스케줄링에 개입하는 과정이 필요하지 않다는 것 & 일시 중단은 사용자가 제어할 수 있음

 

< 코루틴의 주요 패키지; kotlinx.coroutines.* >

  • common 패키지
    • launch / async : 코루틴 빌더
    • Job / Deferred : cancellation 지원
    • Dispatchers : Default는 백그라운드 코루틴을 위한 것이고 Main은 Android, Swing, JavaFx를 위해 사용됨
    • delay / yield : 상위 레벨 지연(suspending) 함수
    • Channel / Mutex : 통신과 동기화를 위한 기능
    • coroutineScope / supervisorScope : 범위 빌더
    • select : 표현식 지원
  • core 패키지
    • CommonPool : 코루틴 문맥
    • produce / actor : 코루틴 빌더

 


 

launch와 async

fun main() { 
    // 메인 스레드
    GlobalScope.launch {  // 새로운 코루틴을 백그라운드에 실행
        delay(1000L)  // 넌블로킹 1초 지연
        println("World!")  // 지연 후 출력
    }
    println("Hello,")  // 메인 스레드의 코루틴이 지연되는 동안 실행
    Thread.sleep(2000L)  // 메인 스레드가 JVM에서 바로 종료되지 않게 2초 기다림
}
  • 메인 스레드 - main() 함수 블록
  • 코루틴 코드는 메인 스레드와 별도로 실행되는 넌블로킹 코드

코루틴에서 사용되는 함수는 suspend()로 선언된 지연함수여야 코루틴 기능을 사용할 수 있음

suspend 키워드를 사용함으로서 이 함수는 실행이 일시 중단(suspended)될 수 있으며 필요에 따라 다시 재개(resume)할 수 있음

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

👆 suspend 함수는 코루틴 블록 안에서만 사용가능함

(코루틴 빌더인 launch와 async나 또 다른 지연함수에서만 사용할 수 있음)

→ 컴파일러가 suspend가 붙은 함수를 자동적으로 추출해 Continuation 클래스로부터 분리된 루틴을 만듦

 

launch 코루틴 빌더 생성

launch를 통해 코루틴 블록을 만들어내는 것

import kotlinx.coroutines.*

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

// Hello,
// job.isActive: true, complete: false
// World!
// job.isActive: false, complete: true
  • launch는 현재 스레드를 차단하지 않고 새로운 코루틴을 실행할 수 있게함
  • 특정 결과값 없이 Job객체를 반환함
  • GlobalScope는 생명주기가 프로그램의 생명 주기에 의존되므로 main()이 종료되면 같이 종료됨

내부적으로 비동기 코드로 동시에 작동할 수 있지만

코드만 봤을 때는 순차적으로 실행되는 것처럼 표현하여 프로그래밍의 복잡도를 낮출 수 있음

 

 

😎 Job 객체란?

Job은 백그라운드에서 실행하는 작업을 가리킴

간단한 생명주기를 가지고 있고 부모-자식 관계가 형성되면 부모의 작업이 취소될 때 하위 자식의 작업이 모두 취소됨

보통 Job() 팩토리 함수나 launch에 의해 job 객체가 생성됨

 

상태 isActive isCompleted isCancelled
  New false false false
  Active true false false
  Completing true false false
  Cancelling false false true
  Cancelled (최종 상태) false true true
  Completed (최종 상태) false true false

  • 보통 Job이 생성되면 활성화된 상태인 Active
  • Job() 팩토리 함수의 인자로 CoroutineStart.Lazy(자세한 설명은 아래에)를 설정하면 활성화되지 않고 New
  • Active 상태로 만들기 위해선 start()나 join()
  • cancel()을 이용하면 Cancelling 상태로 즉시 바뀌고 이후 Cancelled 상태로 바뀜

 

async  코루틴 빌더 생성

launch와 다른 점은 Deffered<T>를 통해 결과값을 반환하는 것

private fun worksInParallel() {
    val one = GlobalScope.async {
        doWork1()
    }
    val two = GlobalScope.async {
        doWork2()
    }
    
    GlobalScope.launch {
        val combined = one.await() + "_" + two.await()
        println("Kotlin Combined : $combined")
    }
}
  • doWork1()과 doWork2()는 async에 의해 완전히 병행 수행
  • await() : 지연된 결과값을 받기 위해 사용할 수 있음
    • 태스크가 종료되는 시점을 기다렸다가 결과를 받을 수 있음
    • 현재 스레드의 블로킹없이 먼저 종료되면 결과를 가져올 수 있음

안드로이드 UI 스레드에서 블로킹 가능성이 있는 코드를 사용하면 앱이 중단되거나 멈추는 경우가 있는데,

await()을 사용하면 UI를 제외한 루틴만 블로킹되므로 UI가 멈추는 경우를 해결할 수 있음

 

코루틴 문맥; CoroutineContext

코루틴이 실행될 때 여러 가지 문맥은 CoroutineContext에 의해 정의됨

내부적으로 CommonPool이 지정되어 코루틴이 사용할 스레드의 공동 풀을 사용함

이미 초기화되어 있는 스레드 중 하나 혹은 그 이상이 선택되며 초기화하기 때문에 스레드를 생성하는 오버헤드가 없는 빠른 기법

*오버헤드(overhead) : 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간, 메모리 등

→ 하나의 스레드에 다수의 코루틴이 동작할 수 있음

 

 


 

시작 시점에 대한 속성

필요한 경우 launch()나 async()에 인자를 지정해 코루틴에 필요한 속성을 줄 수 있음

public fun kotlinx.coroutines.CoroutineScope.launch(
			context: kotlin.coroutines.CoroutineContext /* = compiled code */, 
                        start: kotlinx.coroutines.CoroutineStart /* = compiled code */, 
                        block: suspend kotlinx.coroutines.CoroutineScope.() -> kotlin.Unit): kotlinx.coroutines.Job { /* compiled code */ }

context 매개변수 이외에도 start 매개변수를 지정할 수 있음

  • DEFAULT : 즉시 시작
  • LAZY : 코루틴을 느리게 시작(처음에는 중단된 상태이며 start()나 awit() 등으로 시작)
  • ATOMIC : 최적화된 방법으로 시작
  • UNDISPATCHED : 분산 처리 방법으로 시작
public fun <T> kotlinx.coroutines.CoroutineScope.async(
                        context: kotlin.coroutines.CoroutineContext /* = compiled code */, 
                        start: kotlinx.coroutines.CoroutineStart /* = compiled code */, 
                        block: suspend kotlinx.coroutines.CoroutineScope.() -> T): kotlinx.coroutines.Deferred<T> { /* compiled code */ }

async() 함수도 start 매개변수를 지정하여 시작 시점을 조절할 수 있음

CoroutineStart.Lazy를 사용하면 코루틴 함수를 호출하거나 await() 함수를 호출하는 시점에 async()가 실행되도록 할 수 있음

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

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

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


fun main() = runBlocking {
    val time = measureTimeMillis { 
        val one = async(start = CoroutineStart.LAZY ) { doWork1() }
        val two = async(start = CoroutineStart.LAZY) { doWork2() }
        println("AWAIT: ${one.await() + "_" + two.await()}")
    }
    println("Completed in $time ms")
}

 

runBlocking

새로운 코루틴을 실행하고 완료되기 전까지 현재 스레드를 블로킹

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { 
    launch { 
        delay(1000L)
        println("World!")
    }
    println("Hello")
}
  • 메인 스레드 자체를 잡아두기 위해 main() 함수 자체를 블로킹 모드에서 실행할 수 있음
  • main() 내부의 코루틴이 모두 작동할 때까지 delay() 함수를 사용하여 기다리지 않아도 자동적으로 블로킹
  • 자료형 <Unit>은 생략할 수 있음

코틀린 1.3 버전부터 main() 함수에도 suspend 키워드를 지정할 수 있음

suspend fun main() = coroutineScope {

클래스 내의 멤버 메소드에서도 사용할 수 있음

class Block {
    ...
    fun mySuspendMethod() = runBlocking {
        ...
    }
}

 

join() 함수

명시적으로 코루틴 작업이 완료되는 것을 기다리게 하려면 Job객체의 join() 함수를 사용함

launch에서 Job 객체를 반환하기 때문에 이것을 이용하여 main() 함수에서 join()을 호출할 수 있음

fun main() = runBlocking<Unit> { 
    val job = launch { 
        delay(1000L)
        println("World!")
    }
    println("Hello")
    job.join()
}

위의 코드에서 job객체를 이용하여 join()을 명시하지 않아도 runBlocking에 의해 main()은 코루틴이 끝나기 전까지 블로킹됨

하지만 job.cancel()로 작업을 취소한다면 코루틴 작업이 취소되는 것이기 때문에 결과는 Hello만 출력됨

 

 

 

*블로킹과 넌블로킹에 대한 설명은 아래 포스팅 참고!

 

[Kotlin/Coroutine] 코루틴(Coroutine) - 동시성 프로그래밍

모든 내용은 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다. 동시성 프로그래밍 👆 동기와 비동기 동기적(synchronous) : 프로그래밍에서 순서대로 작업을 수행하여 1개의 루틴을 완료하고

junyoung-developer.tistory.com

 

반응형

댓글