[코루틴 # 1 생성과 소멸하기] - lunch( ), sync( ), Job
Language/코루틴

[코루틴 # 1 생성과 소멸하기] - lunch( ), sync( ), Job

728x90
반응형

코루틴은 자바의 콜백 대신 비동기 작업을 좀 더 우아하고 효율적으로 처리하는 방법입니다.
Kotlin 코루틴을 사용하면 콜백 기반 코드를, 좀 더 읽기 쉬운 순차적으로 작성된 코드로 변환할 수 있습니다.
Suspend 함수를 사용하여 비동기작업을 순차적으로 코드를 작성할 수 있도록 합니다.

코루틴을 사용하면
프로세스/스레드의 작업을 중단(stopped)하고 다른 루틴을 사용하기위해 문맥교환을 하는 대신,
일시중단(suspended)해서 이런 비용을 줄일 수 있게 된다.
다시말해 일시중단은 사용자가 제어할 수 있기 때문에, 운영체제가 스케줄링에 개입하는 과정이 필요없게 됩니다.

뿐만 아니라 콜백과 달리 코루틴은 예외처리를 좀 더 안전하게 사용할 수 있습니다.
그리고 가장 큰 장점은 유지보수와 유연성을 갖고 있다는 것입니다.

 

코틀린은 스레드 생성과정을 단순화해서 쉽고 간단하게 스레드를 생성할 수 있다. 코틀린에서는 스레드와 스레드 풀을 쉽게 만들 수 있지만, 직접 엑세스하거나 제어하지 않는다는 점을 알아야 한다. 여기서는 CoroutieDispatcher를 만들어야 하는데, 이것은 기본적으로 가용성, 부하, 설정을 기반으로 스레드간에 코루틴을 분산하는 오케스트레이터이다. 디스패처가 만들어지면 이를 사용하는 코루틴을 시작할 수 있다.

 

프로세스 vs 스레드 vs 코루틴 

 

프로세스란 실행 중인 어플리케이션의 인스턴스이다. 어플리케이션은 여러 프로세스로 구성될 수 있다. 프로세스는 상태를 가지고 있고 리소스를 여는 핸들, 데이터, 네트워크 연결 등은 프로세스 상태의 일부이며 해당 프로세스 내부의 스레드가 엑세스를 할 수 있다.

 

스레드란 프로세스가 실행할 일련의 명령을 포함한다. 그래서 프로세스는 최소한 하나의 스레드를 포함하며 이 스레드는 어플리케이션의 진입점을 실행하기 위해 생성된다. 스레드 안에서 명령은 한 번에 하나씩 실행되어 스레드가 Block이 되면, Block이 끝날 때까지 같은 스레드에서 다른 명령을 실행할 수 없다.

 

코루틴 경량 스레드 라고 하며, 스레드와 비슷한 라이프사이클을 가지고 있다. 코루틴은 스레드 안에서 실행된다. 스레드 하나에 많은 코루틴이 있을 수 있지만 주어진 시간에 하나의 스레드에서 하나의 명령만이 실행될 수 있다. 예시로, 같은 스레드에 10개의 코루틴이 있다면, 해당 시점에는 하나의 코루틴만 실행된다.

 

 

스레드와 코루틴의 가장 큰 차이점은, 코루틴은 빠르고 적은 비용으로 생성할 수 있다는 것이다. 수천개의 코루틴을 쉽게 생성할 수 있으며 수천개의 스레드를 생성하는 것보다 빠르고 자원도 적게 사용된다.

 

코루틴은 특정 스레드안에서 실행되더라도, 스레드와 묶이지 않는다는 점을 이해해야한다. 코루틴의 일부를 특정 스레드에서 실행하고 실행을 중지한 다음 나중에 다른 스레드에서 계속 실행하는 것이 가능하다. 코틀린이 실행 가능한 스레드로 코루틴을 이동시키기 때문이다. 각 코루틴은 특정 스레드에서 시작되지만 어느 시점이 지나면 다른 스레드에서 다시 시작된다. 

 

 


비동기 작업이란?

더보기

비동기 작업을 예를들면, 조사가 필요한 질문이 있는데 동료에게 답을 찾도록 정중하게 요청한다고 가정해 보겠습니다. 그러면 동료가 스스로 작업을 시작합니다. 동료가 대답을 할 때까지 대답에 의존하지 않는 다른 관련 없는 작업을 계속할 수 있습니다. 이 예에서 동료는 "별도의 스레드에서"비동기 적으로 작업을 수행합니다.

 

스레드가 Blocked(차단)되면 다른 스레드에서 실행한 결과를 받기 전까지 현재 스레드에서는 다른 작업을 할 수 없게됩니다. 반면 스레드가 Suspend(중단)된다면 결과를 받을 때까지 다른 작업(로딩 화면이 돌아간다던지..)이 진행될 수 있습니다.

다시 말해 Blocked 이 아닌 Non-Blocked 되기 때문에 코 루틴이 메인 또는 UI 스레드의 진행을 차단하거나 방해하지 않습니다. 따라서 코 루틴을 사용하면 Main(UI) 스레드에서 실행되는 UI 상호 작용이 항상 우선순위를 갖기 때문에 사용자가 가능한 가장 원활한 경험을 가질 수 있습니다.

 

 

 

동시성 프로그래밍 이란?

더보기

병렬성은 실제로 동시에 여러 작업을 처리되는 것이고,

동시성은 한번에 하나의 일만 처리하지만,

잦은 스위칭이 일어나면서 여러 일을 처리하기 때문에 동시에 여러 작업이 처리되는 것처럼 보인다.

 

올바른 동시성 코드는 결정론적인 결과를 갖지만 실행 순서에는 약간의 가변성을 허용하는 것이다.

 

* 결정론적(특정 입력이 들어오면 언제나 똑같은 과정을 거쳐서 항상 똑같은 결과를 내놓는다)

 

 

동시성을 이해하는 가장 좋은 방법은 순차적인 코드를 동시성과 비교하는 것이다.
먼저 아래와 같은 비동시성 코드가 있다고 가정해보자.

 

fun getProfile(id: Int): Profile {
  val basicUserInfo = getUserInfo(id)
  val contactInfo = getContactInfo(id)

  return createProfile(basicUserInfo, contactInfo)
}

 

사용자의 정보 (userInfo)  연락처 정보 (contactInfo) 를 순차적으로 실행하고, 프로필을 만들게 된다. 하지만 이러한 코드의 문제점이 두가지가 존재한다.
getUserInfo  getContactInfo 가 둘다 웹 서비스를 호출하고, 반환하는데 1초이상 소요된다면 getProfile은 항상 2초 이상 걸릴 것이다.
이 때 getUserInfo  getContactInfo 는 서로 의존적이지 않기 때문에 이들을 동시에 호출한다면, getProfile 의 실행시간을 절반으로 줄일 수 있을 것이다.

suspend fun getProfile(id: Int) {
  val basicUserInfo = asyncGetUserInfo(id)
  val contactInfo = asyncGetContactInfo(id)

  createProfile(basicUserInfo.await(), contactInfo.await())
}

 코루틴을 이용해서, 비동기적으로 변경한 모습이며 위 코드는 두 요청이 거의 동시에 시작될 것이다. 두 함수의 await() 를 호출하여 둘다 완료될 때에만 createProfile 을 실행하도록 한다면, 어떤 호출이 먼저 종료되는지에 관계없이 getProfile의 결과가 결정론적임을 보장할 수 있다. 

 

 

 


코루틴 Common / Core 패키지


1) Common 패키지

기능 설명
launch / async 코루틴 빌더
Job / Deferred cancellation 지원
Dispatchers - Default는 백그라운드 코루틴을 위한 것이고
- Main 은 Android나 Swing, JavaFx를 위해 사용
delay/yield 상위 레벨 지연(suspending)함수
Channel / Mutex 통신과 동기화를 위한 기능
coroutineScope / supervisorScope 범위 빌더
select 표신식 지원


2) Core 패키지

기능 설명
CommonPool 코루틴 문멕
produce / actor 코루틴 빌더

 


코루틴 첫 예제 코드

import kotlinx.coroutines.*

 
fun main() { // ================ << 메인 스레드 문맥 >>  ================ 

    GlobalScope.launch { // ================ << 코루틴 문맥 >> ================ 
    // 새로운 코루틴을 백그라운드 (다른 스레드)에서 실행
        delay(1000L) // 1초의 넌블로킹 지연 (시간의 기본단위는 ms)
        Thread.sleep(1000L)
        println("World!") // 지연 후 출력
        doSomething() // 지연함수 호출
        
    } // 코루틴 문맥 끝
    
    println("Hello,") // main 스레드가 코루틴이 지연되는 동안 계속 실행된다.
    Thread.sleep(2000L) // main스레드가 JVM에서 바로 종료되지 않게 2초 기다린다.
    //delay(2000L)
    
}// 메인 스레드 문맥 끝
// <<<<< 코루틴 지연함수 >>>>>
suspend fun doSomething() {
    println("Do something!")
}

 

코루틴 블록

  • 우리가 지금까지 사용한 main( ) 함수 의 블록은 메인 스레드로써 작동하게 된다.
  • 이때 코루틴 블록을 지정하면, 지정된 코루틴 블록은 메인 스레드가 아닌 백그라운드에서 실행하게된다. 
  • 따라서 메인스레드와 별도로 실행되므로 넌 블로킹 코드이기도 하다. 

 

지연함수(suspend)

  • 코루틴 (블록 내부)에서 사용되는 함수는 suspend( ) 로 선언된 지연함수 여야 코루틴 기능을 사용할 수 있다. 
  • 함수 앞에 suspend를 표기함으로서 이 함수는 실행이 일시중단(suspended)될 수 있으며, 필요한곳에서 다시 재개(resume)할 수 있게 된다. 
  • delay( ) 함수나 yield( ) 함수와 같이 상위 레벨 지연함수도 있고, 사용자 함수에 suspend 붙여 직접 지연함수를 선언하여 코루틴에서 사용할 수 도 이따. 
  • 컴파일러는 suspend 가 붙은 함수를 자동적으로 추출해 continuation 클래스로부터 분리된 루틴을 만든다. 
  • 지연함수는 코루틴 빌더인 launch와 async에서 사용할 수 있지만 메인 스레드에서는 사용할 수 없다. 
  • 지연함수는 또 다른 지연함수 내에서 사용하거나 코루틴 블록에서만 사용해야 한다. 

 


코루틴 빌더 생성하기 : launch( ) / async( )


- 코루틴 블록을 만들어 내는것을 코루틴 빌더의 생성이라고 한다.

- launch()와 async() 를 통해 코루틴 블록을 만들어 낼 수 있다. 

 

1) launch( )로 코루틴 빌더 생성하기

launch는 현재 스레드를 차단하지 않고 새로운 코루틴을 실행할 수 있게 하며

특정 결괏값 없이 Job객체를 반환합니다. 

 

Job 객체를 받아 코루틴의 상태를 출력해보면 다음과 같다. 

import kotlinx.coroutines.*

fun main() {
    val job = GlobalScope.launch { // Job 객체의 반환
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    println("job.isActive: ${job.isActive}, completed: ${job.isCompleted}")
    Thread.sleep(2000L)
    println("job.isActive: ${job.isActive}, completed: ${job.isCompleted}")
}

launch를 살펴보면 실행범위를 결정하는 GlobalScope가 지정되어 있습니다.

이것은 코루틴의 생명주기가 프로그램의 생명주기에 의존되므로 main()이 종료되면 같이 종료됩니다. 

코루틴을 실행하기 위해서는 내부적으로 시레드를 통해서 실행될 수 있습니다. 

단 실행 루틴이 많지 않은 경우에는 내부적으로 하나의 스레드에서 여러 개의 코루틴을 실행할 수 있기 때문에 1개의 스레드면 충분합니다. 

 

 

지연함수(suspended) 함수 2개를 만들고 delay()를 사용하여 코루틴을 실행을 해보겠습니다.

import kotlinx.coroutines.*

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

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

private fun worksInSerial() {
    // 순차적 실행: 순차 표현이지만 내부적으로는 비동기 코드로서 동시에 작동됨
    GlobalScope.launch {
        val one = doWork1()
        val two = doWork2()
        println("Kotlin One : $one")
        println("Kotlin Two : $two")
    }
}

fun main() {
    worksInSerial()
    readLine() // main이 먼저 종료되는 것을 방지하기 
}

 

suspend 키워드를 사용한 두개의 함수 doWork1( ) 과 doWork2( ) 를 정의하고 그안에 시간이 다른  delay( ) 를 사용했습니다. launch에 정의된 doWork1( ) 과 doWork2( ) 함수는 순차적으로 표현할수 있습니다.

2개의 함수는 내부적으로 비동기코드로서 동시에 작동할 수 있지만

코드만 봤을 때는 순차적으로 실행되는 것처럼 표현함으로서 프로그래밍 복잡도를 낮추게 됩니다. 

 

 

 

2) async( )로 코루틴 빌더 생성하기

async도 launch 와 같이 새로운 코루틴을 실행할 수 있다. 

다른점은 Deferred<T>를 통해 결괏값을 반환한다는 것이다. 

이때 지연된 결괏값을 받기위해서 await()을 사용할 수도 있다.

 

import kotlinx.coroutines.*

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

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

private fun worksInParallel() {
    // Deferred<T> 를 통해 결과값을 반환
    val one = GlobalScope.async {
        doWork1()
    }
    val two = GlobalScope.async {
        doWork2()
    }

    GlobalScope.launch {
        val combined = one.await() + "_" + two.await()
        println("Kotlin Combined : $combined")
    }
}


fun main() {
    worksInParallel()
    readLine()
}

doWork1( ) 과 doWork2( )는 async에 의해 감싸져 있으므로 완전히 병행 수행할 수 있습니다. 

여기서는 delay( )로 1초만 지연시킨 doWork1( ) 이 먼저 종료되리라 예측할 수 있습니다. 

 

그러나 더 복잡한 루틴을 작성하는 경우에는 많은 태스크들과 같이 병행 수행되므로 어떤 루틴이 먼저 종료될지 알기 어렵습니다. 따라서 테스크가 종료되는 시점을 기다렸다가 결과를 받을 수 있도록 await( )를 사용해 현재 스레드의 블로킹 없이 먼저 종료되면 결과를 가져올 수 있습니다. 여기서는 combined라는 변수에 2개의 비동기 루틴이 종료되고 결과가 반환되면 문자를 합쳐서 할당합니다. 

 

 


코루틴 시작 시점 조절하기  


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

 

launch() 함수 인자(매개변수)를 지정해 시작시점을 조절

 lunch() 함수 선언부(원형)
public fun launch(
    context: CoroutineContext, 
    start: CoroutineStart,
    parent: Job?,
    onCompletion: CompletionHandler?,
    block:suspend CoroutineScope.() -> Unit
    ): Job {
    
    //...
}​

 

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

CoroutineStart는 다음과 같은 시작 방법을 정의할 수 있다.

  • DEFAULT: 즉시시작
  • LAZY: 코루틴을 느리게 시작(처음에는 중단상태이며, start()나 await()등으로 시작됨)
  • ATOMIC: 최적화된 방법으로 시작
  • UNDISPATCHED: 분산 처리 방법으로 시작

예를들어 매개변수 이름을 사용해 다음과 같이 지정하면 start()혹은 await()이 호출될때 실제 루틴이 시작된다.

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

// ...

job.start() // 실제 시작지점 

 

 

async() 함수 인자(매개변수)를 지정해 시작시점을 조절

 

async 에서 기본 인수는 문맥을 지정할 수 있는데 문맥 이외에도 몇가지 매개변수를 더 정할 수 있다. 

 

async() 함수 선언부(원형)
public fun <T> async(
	context: CoroutineContext, 
	start: CoroutineStart, 
	parent: Job?,
	onCompletion: CompletationHandler?, 
	block:suspendCoroutinScope.() ->T
    ): kotlinx.coroutines.experimental.Defferred<T> { 
    
    //...
} ​


여기서  start  매개변수를 사용하면 async() 함수의 시작 시점을 조절할 수 있다. 

예를들어 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")

    launch(Dispatchers.Default) {

    }

    async(Dispatchers.Default) {

    }

}

 

 


코루틴 블로킹 모드로 실행하기 : runBlocking( ) , join( ) 


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

다음과 같이 runBlocking에서는 지연함수를 사용할 수 있습니다. 

이것은 블록을 2초정도 잡아놓는다. 

runBlocking {
	delay(2000)
}

 

 

main()을 블로킹 모드로 동작시키기: runBlocking( )

- runBlocking을 통해 새로운 코루틴을 실행하고 완료하기 전까지 현재 스레드를 블로킹할 수(잡아둘 수) 있다. 

 

 

메인 스레드 자체를 잡아두기위해 다음과 같이 main( ) 함수 자체를 블로킹 모드에서 실행할 수 있다. 

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // Option 1. 메인 메서드가 코루틴 환경에서 실행
    
    launch { // 백그라운드로 코루틴 실행
        delay(1000L)
        println("World!")
    }
    
    println("Hello") // 즉시 이어서 실행됨
    //delay(2000L)     // delay()를 사용하지 않아도 코루틴을 기다림
    
}

 

 

명시적으로 코루틴 블로킹 하기: join( ) 

- Job 객체의 join( ) 함수를 사용하여 명시적으로 코루틴의 작업이 완료되는 것을 기다릴 수 있다. 

 

import kotlinx.coroutines.*

suspend fun main() = coroutineScope { // Option 2. 코틀린 1.3 부터는 main()에 suspend 지정 가능
    
    val job = launch { // 백그라운드로 코루틴 실행
        delay(1000L)
        println("World!")
    }
    
    println("Hello") // 즉시 이어서 실행됨
    job.join()  // 명시적으로 코루틴이 완료되길 기다린다. 취소할 경우 job.cancel()을 사용한다.
}

Job 객체


  • job은 백그라운드에서 실행하는 작업을 가르킵니다.
  • 보통 Job() 팩토리 함수나, lunch에 의해 job 객체가 생성됩니다.
  • 모든 코루틴에는 job이 있으며 이 job을 사용하여 코루틴을 취소 할 수 있습니다.
  • job은 개념적으로 간단한 생명주기를 가지고 있고, 상위-하위 계층(부모-자식 관계)으로 정렬될 수 있는데, 상위 job을 취소하면 job의 모든 하위 job이 즉시 취소되므로 각 코루틴을 수동으로 취소하는 것보다 훨씬 편리합니다.
  • Fire and Forget 작업이다. 한번 시작된 작업은 예외가 발생하지 않는 한 대기하지 않는다. 기본적으로 job 내부에서 발생하는 예외는 job을 생성한 곳까지 전파되기 때문에, 완료되기를 기다리지 않아도 발생한다.

Job  객체의 상태

  • job 객체는 다음의 상태를 갖습니다. 
  • job 의 상태를 판별하기 위해 job에는 isActive, isCompleted, isCancelled 변수가 있습니다.

job의 상태

 

1. New (생성)

job은 기본적으로 launch()나 job()을 사용해 생성될 때 자동으로 시작되지만,(보통 job 이 생성되면 Active 상태를 가짐)자동으로 시작되지 않게 하려면 job팩토리 함수에 인지로 CoroutineStart.Lazy 를 사용 하면 아직은 job을 활성화하지 않고 New 상태로 만들어진다. 

 

2. Active (활성)

활성상태에 있는 job은 다양한 방법으로 시작할 수 있지만 일반적으로 start(), join()을 이용해서 실행하는데,

둘의 차이점은 start()의 경우 job 이 완료될 때까지 기다리지 않고 job을 시작하는 반면,

join()은 job이 완료될 때까지 일시 중단한다는 점이다. 그래서 start()의 경우는 suspend 함수에서 호출하지 않아도 되고, join()의 경우 실행을 일시중단할 수 있기 때문에 suspend함수 내부에서 호출해야 한다. 

 

 

3. Cancelling(취소중)

cancel()함수로 취소요청을 받은 활성 job은 취소중이라는 스테이징 상태로 들어갈 수 있다. 

 

 

4. Cancelled(취소됨)

취소 또는 처리되지 않은 예외로 인해 실행이 종료된 job은 취소됨으로 간주한다.

 

 

5.  Completing(완료중)

 

 

6. Completed(완료됨)

 

 

 

코루틴(job)의 라이프 사이클 (Fire and Forget 패턴)

 

job 함수

- job을 통해 할 수 있는 것은 아래와 같습니다. 

  • start( )  : 현재의 coroutine의 동작 상태를 체크하며, 동작 중인 경우 true, 준비 또는 완료 상태이면 false를 return 한다.
  • join( ) : 현재의 coroutine 동작이 끝날 때까지 대기한다. 다시 말하면 async {} await처럼 사용할 수 있다.
  • cancel( ) : 현재 coroutine을 즉시 종료하도록 유도만 하고 대기하지 않는다. 다만 타이트하게 동작하는 단순 루프에서는 delay가 없다면 종료하지 못한다.
  • cancelAndJoin( ) : 현재 coroutine에 종료하라는 신호를 보내고, 정상 종료할 때까지 대기한다.
  • cancelChildren( ) : CoroutineScope 내에 작성한 children coroutine들을 종료한다. cancel과 다르게 하위 아이템들만 종료하며, 부모는 취소하지 않는다.

 

job의 상태는 한 방향으로만 이동

runBlocking {
  val job = CoroutineScope.launch {
    delay(2000)
  }

  job.join()

  //Restart
  job.start()
  job.join()
}

Job은 특정 상태에 도달하면 이전 상태로 되돌아가지 않는다. 

위 코드에서 처음 호출한 job.join()이 완료되면 "Completed(완료됨)" 상태에 도달했으로 start()를 호출해도 아무런 변화가 없다. 

 

코루틴이 무거운 작업을 하고 있을때에는 job.cancel()이 작동하지 않는다. 

이때 주기적으로 yeild()나 isActive로 코루틴에게 작업을 취소할 수 있는 여지를 제공한다.

 

자식이 Exception을 뱉어버리면 전역으로 퍼지게 되어 일을 중단하게 되는데 Exception이 발생하게 될 때 부모에게는 퍼지지 않게 하기 위해 SupervisorJob()을 사용하게 된다. 그렇다면, 자식들이 취소되어도 다른 자식들은 이어서 일을 진행할 수 있다. 






참고

https://velog.io/@jshme/kotlin-coroutines-basic
developer.android.com/codelabs/kotlin-android-training-coroutines-and-room?index=..%2F..android-kotlin-fundamentals#4
구글 예제 bluprintsunflower 앱의 비동기처리를 coroutine으로구현
wooooooak.github.io/kotlin/2019/08/25/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EA%B0%9C%EB%85%90-%EC%9D%B5%ED%9E%88%EA%B8%B0/
https://thdev.tech/kotlin/2019/04/08/Init-Coroutines-Job/
Do it 코틀린 책 참고
구글 코루틴 연습 사이트 developer.android.com/courses/pathways/android-coroutines?hl=ko

728x90
반응형