Kotlin Coroutine

siwan·2021년 12월 22일
1

코틀린 코루틴

목록 보기
1/1

Coroutine이란

coroutine은 kotlin 언어를 쓰는 개발자들이 겪는 스레딩 문제를 직관적인 방식으로 해결 할 수 있는 기술 이다.

Corotuine은 LightWeight Thread 이다.

기존 자바의 async task 또는 rxjava 등 비동기 작업을 하기 위해서는 하나의 쓰레드를 생성해야 하는데,Coroutine은 쓰레드 스케쥴링 가능한 코드 블록 또는 코드블록들의 집합내에서 스위칭이 이뤄지기 때문에 OS단의 context switching이 일어나는게 아닌 Coroutine Object 객체들만이 스위칭함으로써 부하가 덜 생긴다.

Thread

  • Task 단위 = Thread
    다수의 작업 각각에 Thread를 할당 한다.
  • Context Switching
    OS커널에 의한 Context Switching을 통해 동시성을 보장한다.
    Blocking: 작업 1(Thread) 이 작업 2(Thread) 의 결과가 나오기까지 기다려야한다면
    작업 1 Thread 는 Blocking 되어 그 시간동안 해당 자원을 사용하지 못한다.

Coroutine

  • Task 단위 = Obejct(Coroutine)
    다수의 작업 각각에 Object를 할당 한다.
    이 Coroutine Object는 객체를 담는 JVM Heap에 적재 된다.
  • Progreammer Switching = No Context Switching
    프로그래머의 코딩을 통해 Switching 시점을 마음대로 정함으로써 동시성을 보장한다.
    Suspend(Non_Blocking) : 작업 1(Object) 이 작업 2(Object) 의 결과가 나오기까지 기다려야한다면 작업 1 Object 는 Suspend 되지만 작업 1 을 수행하던 Thread 는 그대로 유효하기 때문에 작업 2 도 작업 1 과 동일한 Thread 에서 실행될 수 있습니다.

Coroutine Context란

Kotlin 표준 라이브러리에 정의된 CoroutineContext 타입의 값으로 정의된 어떤 context에서 실행된다.

CoroutineScope

코루틴의 범위, 코루틴 블록을 묶음으로 제어할수 있는 단위

GlobalScope 는 CoroutineScope 의 한 종류입니다. 미리 정의된 방식으로 프로그램 전반에 걸쳐 백그라운드 에서 동작 한다.

Dispachers

코루틴의 실행을 특정 스레드로 제한하거나, 스레드 풀에 보내거나, 제한을 받지 않는 상태로 실행할 수 있다.

Dispatchers.Default : CPU 사용량이 많은 작업에 사용합니다. 주 스레드에서 작업하기에는 너무 긴 작업 들에게 알맞습니다.
Dispatchers.IO : 네트워크, 디스크 사용 할때 사용합니다. 파일 읽고, 쓰고, 소켓을 읽고, 쓰고 작업을 멈추는것에 최적화되어 있습니다.
Dispatchers.Main : 안드로이드의 경우 UI 스레드를 사용합니다.

코루틴 쓰는 방법은?

1. 사용할 Dispatcher 를 결정하고
2. Dispatcher 를 이용해서 CoroutineScope 만들고
3. CoroutineScope 의 launch 또는 async 에 수행할 코드 블록을 넘기면 됩니다.

ex )_다음 예시에서 내부 코루틴 블록은 멈추지 않습니다.

val scope= CoroutineScope(Dispachers.Main)

val job = scope.launch {

//새로운 Scope 생성
CoroutineScope.launch(Dispachers.Main){

// 외부 코루틴이 취소 되어도 끝까지 돌림

  }

}

job.cancel()

외부 코루틴 블록 과 내부 코루틴 블록은 서로 제어범위가 달라진다.

Job 객체의 cancel() 메서드는 자신이 해당하는 CoroutineScope 의 코루틴 블록을 취소시켜 멈출수 있지만, 내부 코루틴 블록은 다른 CoroutineScope 로 분리 되었기 때문에 멈출수 없다.

코루틴 블록이 멈춰도, 내부 코루틴 블록은 끝까지 수행 됨

코루틴 제어를 위한 주요 키워드

-launch, async
-runBlocking
-Job, Deferred

launch는 job 객체를 반환 한다.

val job : Job = launch {
    ...
}

반환받은 Job 객체로 코루틴 블록을 취소하거나, 다음 작업의 수행전 코루틴 블록이 완료 되기를 기다릴 수 있다.

val job:Job = CoroutineScope(Dispachers.IO).launch {
            var i = 0
            while (i < 10) {
                delay(500)
                i++
            }
        }

        job.join() // 완료 대기
        job.cancel() // 취소

여러개의 코루틴 블록을 완료 대기 시킬 수 있다. ( joinAll(job,job) )

val job1: Job = CoroutineScope(Dispatchers.IO).launch {
            var i = 0
            while (i < 10) {
                delay(500)
                println("job1 state = ${this.coroutineContext.job} i=$i")
                i++
            }
        }

val job2: Job = CoroutineScope(Dispatchers.IO).launch {
            var i = 0
            while (i < 10) {
                delay(500)
                println("job2 state = ${this.coroutineContext.job} i=$i")
                i++
            }
        }

        runBlocking {
            joinAll(job1, job2)
            job1.cancel()
            job2.cancel()

            println("${job1.job}")
            println("${job2.job}")

        }

위와 같은 예제로 job1 코루틴 context 객체를 받아서 돌릴 수 있다.

val job1: Job = CoroutineScope(Dispatchers.IO).launch {
            var i = 0
            while (i < 10) {
                delay(500)
                println("job1 state = ${this.coroutineContext.job} i=$i")
                i++
            }
        }

        CoroutineScope().launch(job1) { //
            var i = 0
            while (i < 10) {
                delay(500)
                println("job2 state = ${this.coroutineContext.job} i=$i")
                i++
            }
        }

        runBlocking {
            job1.join()

        }

async() — Deferred

async() 함수로 시작된 코루틴 블록은 Deferred 객체를 반환한다.

val deferred : Deferred<T> = async{
...
    T // 결과값
}

Deferred 객체를 이용해 제어가 가능하며 동시에 코루틴 블록에서 계산된 결과값을 반환 한다.

val deferred: Deferred<Int> = CoroutineScope(Dispatchers.IO).async {
            var i = 0
            var sum = 0;
            while (i < 10) {
                sum += i
                i++
            }

            sum //결과값 반환
        }

        runBlocking {
            val msg = deferred.await() //값을 반환
            println(msg) // result 출력 45
        }
    }

async 코루틴 블록을 실행할 경우 각각의 Deferred 객체에 대해서 await() 함수로 코루틴 블록이 완료 될때까지 다음 코드 수행을 대기 할 수 있습니다. await() 함수는 코루틴 블록이 완료되면 결과를 반환

val deferred1 = CoroutineScope(Dispatchers.IO).async {
            var i = 0
            while (i < 10) {
                delay(500)
                i++
            }

            "result1"
        }

        val deferred2 = CoroutineScope(Dispatchers.IO).async {
            var i = 0
            while (i < 10) {
                delay(1000)
                i++
            }

            "result2"
        }

        runBlocking {
            val result1 = deferred1.await()
            val result2 = deferred2.await()

            println("$result1 , $result2") // result1 , result 2 출력
        }
awaitAll(deferred1,deferred2) //async를 동시에 돌려 결과를 반환 받는다.

코루틴을 블록을 지연 시킬 수 있다.

**CoroutineStart.LAZY**

example :)
- async(context = Dispatchers.IO, start = CoroutineStart.LAZY)
- launch(context = Dispatchers.IO, start = CoroutineStart.LAZY)

start() , (await(), join())의 차이점

start() 함수는 async 코루틴 블록을 실행 시킬땐 블록의 수행 결과를 반환하지 않습니다. 또한 await(),join 함수와 다르게 코루틴 블록이 완료 되는것을 기다리지 않습니다.

start
lazy async 0
lazy async 1
lazy async 2
lazy async 3
lazy async 4
end
start
end
lazy async 0
lazy async 1
lazy async 2
lazy async 3
lazy async 4

runBlocking()

runBlocking() 함수는 코드 블록이 작업을 완료 하기를 기다린다.

runBlocking {
    ...
}

launch() 함수로 시작된 블록은 join() 함수

async() 함수로 시작된 블록은 await() 함수

runBlocking() 함수로 시작된 블록은 아무런 추가 함수 호출 없이 해당 블록이 완료

  • 주의점 :
  1. runBlocking 코루틴 블록이 사용하는 스레드는 현재 runBlocking() 함수가 호출된 스레드가 된다는 것
  2. 안드로이드일 경우 메인스레드에서 호출 하여 시간이 오래 걸리는 작업을 수행하는 경우 ANR 발생 위험

Coroutine 취소

cancel/cancelAndJoin

job을 취소 할 수 있다.

블록내에서 처리 후 cancel()을 호출 할 수 있다.

Cancellation is cooperative

cancel을 할 경우 coroutine의 모든 suspending functions은 취소를 지원 된다.

runBlocking {
            val startTime = System.currentTimeMillis()
            val job = launch(Dispatchers.Default) {
                var nextPrintTime = startTime
                var i = 0
                while (i < 5) {
                    // computation loop, just wastes CPU //
                    yield() //여기에 추가하면 정상적으로 취소된다 
                    // print a message twice a second
                    if (System.currentTimeMillis() >= nextPrintTime) {
                        println("I'm sleeping ${i++} ...")
                        nextPrintTime += 500L
                    }
                }
            }
            delay(1300L) // delay a bit
            println("main: I'm tired of waiting!")
            job.cancelAndJoin() // cancels the job and waits for its completion
            println("main: Now I can quit.")
        }

하지만 위 코드는 블록내의 코루틴이 아닌 코틀린 시스템내에서 작동하는 코드 이므로 while문 안의

취소 여부와 관계 없이 동작 한다.

취소 할 수 있는 방법은 두가지가 있다.

  • yeild() - 주기적으로 취소를 체크하는 suspend function을 invoke 시킨다.
  • if(isActive) - 명시적으로 조건문을 추가 하여 cancel을 체크 한다.
profile
station3 다방 안드로이드 개발자

0개의 댓글