본문 바로가기
Language/Kotlin

[Kotlin] 코루틴 (Coroutine) #2 - 기본기 2

by 신숭이 2022. 2. 24.

[Kotlin] 코루틴 (Coroutine) #2 - 기본기 2

 

 

첫 번째 코루틴 포스트에서 코루틴 빌더 중 launch만 살펴보았다. launch에 대한 더 자세한 내용과 또다른 빌더인 async를 알아보고 루틴의 작업(Job), 그리고 코루틴의 문맥에 대해 알아보겠다.

 

복습 

1. 코루틴 스코프 : 루틴 작업이 정의되는 코드 영역  ex ) GlobalScope, CoroutineScope

2. 코루틴 빌더 : 코루틴 스코프를 생성하는 빌더  ex ) launch, async, runBlocking

 

이 포스트에서 공부할 것

1. 코루틴 빌더 : async

2. 작업(Job)

3. 코루틴 빌더 매개변수

 

 

 

 

 

코루틴 빌더 : async

 

async는 launch 처럼 코루틴 스코프를 생성하는 코루틴 빌더이다. launch는 작업(Job)을 반환한 반면, async는 비동기 작업의 결과를 반환한다. 정확히는 Suspend 함수의 반환값을 Defferred<T>를 통해 결과로 반환한다는 것이다. 아래의 예시로 살펴보자.

 

fun main() = runBlocking {
    workInParallel()
    delay(3000L)	// 메인스레드 종료를 방지하기위해 3초간 붙잡아둔다
}

suspend fun doWork1(): String{      // 코루틴 블록내에서 사용시, 내부적으로 비동기 코드로 작동함.
    delay(1000)
    return "Work1"
}

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

private fun workInParallel(){
    var one = GlobalScope.async {
        doWork1()
    }
    var two = GlobalScope.async {
        doWork2()
    }

    GlobalScope.launch {
        val combine = one.await() + "_" + two.await()		// await 함수는 결과를 기다린후 반환
        println("Kotlin Combine : $combine")
    }
}
Kotlin Combine : Work1_Work2

 

우선 Suspend 함수 doWork1 은 1초의 딜레이를 가지고, doWork2는 3초의 딜레이를 가진다. 따라서 workInParallel 함수는 코드를 순차적으로 적어놓았지만 코루틴은 병렬적으로 수행되기에 총 3초의 시간이 소요된다. 따라서  메인함수에 3초의 종료방지 딜레이를 두었다. main함수의 코루틴 영역은 runBlocking으로 실행하였기에 해당 스레드를 3초간 붙잡는 것이 가능하다. (async, launch를 글로벌 스코프 영역으로 두었기에 메인함수가 종료되면 함께 종료되므로 결과값을 반환 받을 시간이 없기때문이다.)

 

await 

one 과 two 변수는 async 빌더를 통해 Suspend 함수의 반환 값(String 값)을 받는다. 그러나 Suspend함수는 중단가능하기에 값이 호출시점에 온다고 보장할 수 없다. 또한 병렬 처리되고 있는 코루틴의 수가 많으면 combine 과 같은 문자열 결합은 각기 다른 값 반환 시점으로 문제가 발생할 수 있다. 때문에 await() 메서드는 비동기 루틴의 결과를 기다린 후 값을 사용할 수 있도록 도움을 준다. 모든 결과가 반환되면 그때 문자열을 합쳐 combine에 할당한다. 특히 await() 의 장점은 안드로이드에서 사용할 시, UI 스레드만 제외한 루틴을 블로킹하기에 매우 유용하다. 

 

 

 

 

 

작업(Job)

 

 

코루틴의 작업(Job)은 백그라운드에서 실행하는 작업을 말하며, launch 빌더에 의해 생성된다. 생명주기를 가지고 부모-자식 관계가 있어, 부모 작업이 취소되면 하위 자식 작업들도 모두 취소된다. 작업의 상태는 다음과 같다. Job객체는 isActive, isCompleted, isCancelled 라는 멤버변수를 가지고 있고, 이 변수로 상태를 확인할 수 있다.

작업의 상태(State)

보통 Job이 생성되면 디폴트 상태는 Active이다. 다만 코루틴 문맥에서 지연실행(CououtineStart.LAZY)을 할 시, New 상태로 생성된다. New를 Active로 만들기위해선 job.start()나 job.join() 함수를 사용하면 된다. 작업을 취소하기 위해선 job.cancel() 함수를 사용하면 된다.

 

상태 변화

 

New  →  Active :  job.start(), job.join() 으로 지연된 작업 실행

Active → Completing : 현재 루틴 작업은 완료, 자식 루틴의 작업 완료를 기다리는 중

Completing → Completed : 모든 하위 작업까지 완료

Active → Cancelling : 현재 루틴 작업 중, job.cancel() 호출

Completing → Cancelling : 자식 루틴 작업 중, job.cancel() 호출

Cancelling → Cancelled : 완전히 종료

New → Cancelled : 아직 시작하지 않은 작업을 job.cancel() 로 종료

 

fun main() = runBlocking {	// 부모 코루틴
    val job = launch{	// 자식 코루틴
        delay(1000)
        println("world!")
    }
    println("Hello!")
    job.join()		// 있으나 없으나 똑같은 결과. 명시적으로 적음
}
Hello!
world!

위 코드는 메인 함수를 runBlocking 모드 ( 해당 코루틴 스코프가 모두 작업을 끝내기 전까지 스레드를 막는 코루틴 빌더)로 실행하였기에 launch가 실행되기 전까지 함수는 종료되지 않는다. 다만 직관적으로 이해하기 어려울 수 있기에 명시적으로 job.join()을 적어두어 hello 다음 world가 등장함을 코드에서 알 수 있다.

 

 

 

 

 

코루틴 빌더의 매개변수

 

 

launch는 원래 아래와 같은 매개변수를 갖는다. 여기서 기본적으로 알아야할 몇 가지 매개변수를 알아보겠다.

public fun launch(
    context: CoroutineContext,
    start: CoroutineStart,
    parent: Job?,
    onCompletion: CompletionHandler?,
    block: suspend CoroutineScope.()->Unit):Job
){
    ...
}

 

CoroutineContext

코루틴이 실행될 때 문맥은 CoroutineContext에 의해 정의된다. 보통 launch 와 같은 빌더를 사용할때 매개변수 없이 호출하였는데, 이는 상위 코루틴의 문맥을 따르는 것이 디폴트다.

CoroutineContext는 코루틴 문맥을 정의할 수 있다. Dispatcher.Default로 설정하면, GlobalScope와 동일한 문맥을 사용한다.  

val job = launch(Dispatchers.Default){
    delay(1000)
    println("world!")
}
println("Hello!")

코루틴은 내부적으로 스레드의 공동 풀(Pool)을 사용하며 이미 초기화되어있는 스레드를 하나 이상 선택하여 초기화하기에 스레드 생성 오버헤드가 없는 빠른 기법이다. 만약 스레드의 개수를 직접 지정하고자 한다면, 다음과 같이 문맥을 만들어 지정하면 된다.

val threadPool = Executors.newFixedThreadPool(4)
val MyContext = threadPool.asCoroutineDispatcher()

async(MyContext){
	...
}

또는 새로운 이 루틴만의 스레드를 가지고자 한다면 아래와 같이 작성한다.

launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
    println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}

코루틴의 문맥과 Dispatcher에 대해서 좀 더 자세한 이야기는 기본기의 범위를 넘어서기에 이후 포스트에 이어 작성하겠다.

 

 

 

CoroutineStart

루틴의 시작방식에 대해 인자를 넘겨줄 수 있다. 아래의 예시를 보자

val job = async(start = CoroutineStart.LAZY) { doWork1() }

...

job.start()

LAZY는 지연 시작을 의미하며, 루틴은 New 상태로 만들어진 후 중단되어 있다가, start()await() 메서드에 의해 실행 된다. 루틴 시작 방식은 아래와 같이 4가지 종류가 있다.

CoroutineStart.DEFAULT 기본 값, 루틴 즉시 시작
CoroutineStart.LAZY 지연 시작, start() 나 await() 메서드로 시작 가능
CoroutineStart.ATOMIC 중단 불가능한 모드로 시작(최적화)
CoroutineStart.UNDISPATCHED 분산 처리 방법으로 시작(호출 문맥을 따르다가, 중단점 이후 스레드를 스위치해서 실행)

 

 

 

 

 

 

댓글