본문 바로가기
자바와 코틀린

코루틴 - 코루틴 빌더와 Job

by pius712 2024. 9. 17.

코루틴 상태와 Job

Job

모든 코루틴 빌더는 Job 객체를 생성하고 반환한다.

Job 객체는 코루틴 실행을 추상화한 객체로, Job 을 통해 코루틴의 상태를 추적 및 실행을 제어할 수 있다.

코루틴 상태

코루틴의 상태에 대해 한번 알아보자.

코루틴은 아래와 같은 상태를 가질 수 있다.

  • New : 코루틴 빌더를 통해 코루틴을 생성하면 New 상태가 되며, 지연 실행 옵션을 주지 않으면, active 상태로 전이된다.
  • active: 실행중 상태로, 실제 코루틴을 실행하거나 중단된 상태를 포함한다.
  • completing: 자신의 코루틴이 모두 완료되었으나, 자식 코루틴이 완료되기를 기다리는 상태이다.
  • completed: 자신과 자식 코루틴 모두가 완료된 상태이다.
  • cancelling : 코루틴이 취소요청을 받으면 취소중 상태가 되며, 아직 취소된 것은 아니다.
  • cancelled: 취소 요청을 받은 코루틴이 이에 응하여 코루틴이 취소된 상태를 의미한다.

Job 의 상태

Job 은 내부적으로 상태를 추적할 수 있는 필드를 제공한다.

하지만, 이 Job 의 상태와 코루틴의 상태가 1:1로 매핑되지는 않는다.

  • isActive : 코루틴이 실행 중 상태
  • isCancelled : 코루틴이 취소 중 혹은 취소완료 상태
  • isCompleted : 코루틴이 실행완료된 상태 혹은 취소 완료된 상태

코루틴의 취소

코루틴은 실행을 취소할 수 있다는 특징을 가진다.

만약, 해당 작업자체가 불필요한 상황에서 지속적으로 코루틴이 실행된다면 리소스의 낭비가 될 것이다.

예를들어, 안드로이드에서 이미 화면이동을 하였음에도 불구하고 메인스레드가 이전의 화면의 작업을 지속한다면 불필요한 리소스를 사용하는 것이다.

코루틴의 취소는 cancel() 메서드를 통해서 실행할 수 있다.

fun main() = runBlocking {

	val job = launch {
		while(true){
			println("hi")
			delay(50)
		}
	}
	delay(100)
	job.cancel()
}
>> hi
>> hi
>> 종료

코루틴의 취소는 cacel() 메서드를 호출한다고 바로 수행되지는 않는다.

cancel 호출을 받은 코루틴은 내부적으로 취소 플래그를 가지며, 이 플래그를 활성하여 cancelling 상태로 전이하고, cancelled 상태로 전이된다.

이 때문에, 실제 코루틴의 취소요청 이후에 실행되는 코드는 코루틴이 취소되었다는 것을 보장하지 않는다.

이를 보장하기 위해서는 cancelAndJoin() 메서드를 통해 보장할 수 있다.

fun main() = runBlocking {

	val job = launch {
		while(true){
			println("hi")
			delay(50)
		}
	}
	delay(100)
	job.cancelAndJoin()
	// 아래에서는 취소가 확정된 후에 실행됨
}

또한 취소요청을 받은 코루틴이 중단지점이 없다면, 코루틴이 취소되지 않을 수 있다.

아래 코드처럼, delay 에 주석처리를 한다면 launch 코루틴은 취소요청에 응하지 않는다.

이는 스레드가 interrupt 를 받지 못하는 상황과도 비슷하다고 볼 수 있다.

delay, yield, CoroutineScope.isActive 를 통해서 코루틴은 자신의 취소 여부를 확인할 수 있고,

이를 통해 취소할 수 있다.

fun main() = runBlocking {

	val job = launch {
		while(true){
			println("hi")
			// delay(50) 주석 처리
		}
	}
	delay(100)
	job.cancel()
}

코루틴의 지연 실행 (CoroutineStart.Lazy)

별다른 옵션을 주지 않고, 코루틴 빌더를 통해 코루틴을 실행하면 곧바로 active 상태가 되어 실행할 수 있게 된다.

코루틴에서 코루틴을 생성해두고, 지연 실행하는 방법을 제공한다.

fun main() = runBlocking() {
	val job = launch(start = CoroutineStart.Lazy {
		//
	}
}

위 코드 처럼 생성된 job 은 코루틴을 생성만하고 실제 실행되지는 않는다.

지연 실행하는 코루틴의 경우, 명시적으로 start() 메서드를 호출해주어야 실행될 수 있다.

fun main() = runBlocking() {
	val job = launch(start = CoroutineStart.Lazy {
		//
	}
	job.start()
}

코루틴 빌더

코루틴 빌더는 대표적으로 세가지가 있다.

  • launch
  • runBlocking
  • async

launch

launch 는 코루틴 스코프의 확장함수로, 값을 반환하지 않는 코루틴을 생성한다.

fun main() = runBlocking {

	launch {
		delay(1000)
		println("hello")
	}
	launch {
		delay(1000)
		println("hello")
	}
	launch {
		delay(1000)
		println("hello")
	}
}

// 약 1초 뒤
>> hello
>> hello
>> hello

async

launch 와 비슷하지만 작업의 결과를 반환 특징이 있다.

이때 반환되는 타입은 Deferred 타입인데, 이는 Job 의 서브타입이다.

따라서, luanch 에서 반환된 Job 과 마찬가지로, 코루틴의 상태 추적과 실행을 제어 할 수 있다.

async 값 반환 받기

값을 반환받을 때는 await() 메서드를 통해서 값을 반환받을 수 있다.

await 을 실행하는 동안에는 코루틴은 중단된다.

fun main() = runBlocking() {
    val deferred = async {
        delay(1000)
        return@async 10
    }

    println(deferred.await())
}

위 그림처럼, await 을 호출하는 순간 코루틴이 중단된다.

이러한 특성때문에 await 호출을 하는 코드에서는 주의가 필요하다.

아래와 같은 코드를 작성하면 async 코루틴이 순차적으로 진행된다.

fun main() = runBlocking() {
    val deferred1 = async {
        delay(1000)
        return@async 10
    }
    deferred1.await()
    val deferred2 = async {
        delay(1000)
        return@async 10
    }
		deferred2.await()
}

이를 해결하기 위해서는 어떻게 해야할까?

수정된 코드를 살펴보자.

fun main() = runBlocking() {
    val deferred1 = async {
        delay(1000)
        return@async 10
    }
    val deferred2 = async {
        delay(1200)
        return@async 10
    }
    // *위치 바꿈*
    deferred1.await()
		deferred2.await()
}[

위 코드처럼 수정을하게 되면, runBlocking 이 await 로 중단되는 시점에

이미 2개의 async 가 실행된다. 따라서 async 가 동시에 실행될 수 있다.

 

runBlocking

runBlocking 은 새로운 코루틴을 실행하고, 해당 코루틴 블록이 끝날때까지 스레드를 block 상태로 만든다.

일반적으로, 그냥 작성된 코드와 suspend 함수 스타일로 작성된 코드 사이의 브릿지 역할 을 하게 된다.

runBlocking 과 launch 의 차이

runBlocking 은 launch 와 비슷해보이지만, 호출한 스레드를 차단하는지 여부가 다르다.

runBlocking 으로 코루틴을 호출하는 경우, 호출한 스레드가 차단된다.

호출한 스레드가 차단된다는 것은 runBlocking 과 그 하위 코루틴이 실행되는 동안 다른 작업을 호출 스레드가 하지 못한다는 뜻으로 이해해야한다.

참고로, 이것이 꼭 스레드의 상태가 BLOCKED, WAITING 상태로 전이된다는 것을 의미하지는 않는다.

runBlocking 은 별다른 Dispatcher 옵션을 주지 않으면, runBlocking 내에서 호출 스레드를 점유하여 사용한다. 이 경우, runBlocking 의 코루틴을 호출스레드가 실행하기 때문에 호출 스레드가 runBlocking 코루틴을 실행한다.

참고로 아무런 옵션을 주지 않는 경우 BlockingEventLoopDispatcher 라는 디스패처를 사용하며, 내부적으로 이벤트 루프에 의해서 동작한다.

만약, Dispatcher 옵션을 준다면, 다른 스레드에서 실행을 하고 이때는 호출한 스레드의 상태가 runBlocking 코루틴이 끝날때까지 TIMED_WAITNG 가 된다.

반면에 launch 는 호출한 스레드가 스레드를 차단하지 않으며, launch를 실행할 스레드가 자유로워지면 launch 르 실행할 수 있다.

이 때, 스레드가 자유로워진다는 것은 만약 호출 스레드가 launch 코루틴을 실행한다면 스레드를 양보하는 것을 의미하고, 별도의 Dispatcher 를 사용하면 Dispatcher 내부의 유휴 스레드가 launch 를 실행할 수 있을 때를 의미한다.