[안드로이드] 코루틴 실습

dada·2021년 11월 24일
0

Android

목록 보기
18/24
post-thumbnail

✅코루틴 차근차근~

✔basic

  • Coroutine builder
    • launch
    • runBlocking
  • Scope
    • CoroutineScope
    • GlobalScope
  • Suspend function
    • suspend
    • delay()
    • join()
  • Structured concurrency
fun main() {
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second
        println("World!") // print after delay
    }
    println("Hello") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}
  • GlobalScope는 coroutine scope인데, 미리 준비된 객체로, lifetime이 프로그램 전체인 전역 scope임
  • launch은 coroutine builder로서, 내부적으로 하나의 job을 반환함
  • delay 함수는 suspend function(논블로킹). sleep 은 메인 스레드를 블로킹하는 함수

fun main() {
    GlobalScope.launch {
        delay(1000L) // non-blocking delay for 1 second
        println("World!") // print after delay
    }
    println("Hello") // main thread continues while coroutine is delayed
    runBlocking {
    	delay(2000L) // block main thread for 2 seconds to keep JVM alive
    }
}
  • runBlocking 도 launch처럼 하나의 코루틴 빌더이지만 launch와 차이가 있음
  • launch는 자신을 호출한 쓰레드를 블로킹 시키지 않지만(여기서는 main thread) runBlocking는 자신을 호출한 쓰레드를 블로킹 시킴(여기서 main thread를 2초간 블로킹시킴 ) 따라서 UI를 그리는 main thread에서 runBlocking을 호출하면 안되겠지

fun main() = runBlocking { // this: CoroutineScope
    GlobalScope.launch { // launch a new coroutine and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello") // main coroutine continues while a previous one is delayed
}
  • 위 코드는 runBlocking 을 아래에서만 사용했는데 이번엔 모든 코루틴이 종료된 이후 main 함수가 리턴되길 원함. 이럴경우 코루틴 스코프들을 모두 runBlocking 으로 감싸주면 됨

fun main() = runBlocking { // this: CoroutineScope
    GlobalScope.launch { // launch a new coroutine and continue
        delay(3000L) 
        println("World!") 
    }
    println("Hello")
    delay(2000L)
}
  • 여기서의 결과값은 Hello만 찍힌다 그 이유는 runBlocking 코루틴이 실행되고 그 안에서 GlobalScope가 시작되고 3초간 기다리고 있을 동안 Hello가 찍힌 후 2초 기다렸다가 바로 runBlocking 코루틴이 종료되기 때문이다. delay는 내가 초를 계산해야되는 이런 단점이 있어서 이런 문제를 해결하기 위해 등장한게 join() 이다

fun main() = runBlocking { // this: CoroutineScope
    val job = GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    
    println("Hello")
    job.join()
}
  • launch 라는 코루틴 빌더는 job 객체를 반환한다. job 을 join 하게 되면, delay 를 사용하지 않고, 코루틴이 완료될때까지 기다렸다가 메인함수를 종료하도록 할 수 있다.

fun main() = runBlocking { // this: CoroutineScope
    val job = GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    val job2 = GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    val job3 = GlobalScope.launch {
        delay(1000L)
        println("World!")
    }

    println("Hello")
    job.join()
    job2.join()
    job3.join()
}
  • 그렇다면 위의 예시처럼 코루틴을 여러개 실행하면 여러 job이 생기고 runBlocking 코루틴 안에서 이 모든 job들이 끝날때까지 기다려주는 join을 모든 job에 달아줘야한다 굉장히 싫음.
  • 이런 문제가 발생하는 이유는 GlobalScope 레벨의 코루틴 스코프를 만들어 사용했기 때문임.
  • 왜냐면 top-level 코루틴(GlobalScope)와 runBlocking 코루틴은 구조적으로 관계가 없다. 따라서 GlobalScope 에서 코루틴이 끝나든 말든 상관없이 join이 없으면 runBlocking 코루틴이 끝나고, main함수가 끝나버린다.
  • 그러면.. 서로 구조적으로 관계를 만들어 주면, 서로 끝날때까지 기다려 줄 수 있지 않을까? 이를 위해선 Structured concurrency이란 패러다임을 사용한다
    • Structured concurrency: 구조화 된 동시성은 동시 프로그래밍에 대한 구조화 된 접근 방식을 사용하여 컴퓨터 프로그램의 명확성, 품질 및 개발 시간을 개선하기위한 프로그래밍 패러다임
    • 즉, top level coroutine을 만들지 말고, runBlocking에 의해 생성된 coroutine의 child로 코루틴을 만들면, 부모 코루틴이 child 코루틴 완료될때까지 기다려준다.
      방법은, GlobalScope 에서 launch하지 말고, runBlocking에서 들어온 coroutine scope 에서 launch를 하자. 아래 코드를 보아라
    • 여기서의 this는 runBlocking 의 코루틴 스코프를 가르키는데 람다에서 this는 생략 가능해서 아래 코드에서 this를 생략해도 된다.
fun main() = runBlocking {

    this.launch {
        delay(1000L) 
        println("World!")
    }
    
    this.launch {
        delay(1000L) 
        println("World!")
    }
    
    this.println("Hello")
}

fun main() = runBlocking { // this: CoroutineScope
    launch {
        doWorld()
    }
    println("Hello")
}

// this is your first suspending function
suspend fun doWorld() {
    doBeautiful()
    delay(1000L)
    println("World!")
}

suspend fun doBeautiful() {
    println("beautiful")
}
  • suspend function은 코루틴 안이나, 다른 suspend function 안에서만 호출 가능하고, delay 역시, 코루틴 안이나, suspend function에서만 호출 가능하다.
  • 위 실행 결과는 Hello beautiful World!

fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}
  • 위 코드는 코루틴 10만개를 만들어서 코루틴 하나당 .을 하나씩 찍는다. 근데 잘됨 즉 thread{ Thread.sleep(5000L) } 로 thread만드는거 보다 훨 빠름

fun main() = runBlocking { // this: CoroutineScope
    launch {
        repeat(5) {
            println("Coroutine A, $it")
        }
    }
    launch {
        repeat(5) {
            println("Coroutine B, $it")
        }
    }
    println("Coroutine Outer")
}

//어느 thread에서 찍히는지 보려고 println 오버라이드
fun <T> println(msg: T) {
    kotlin.io.println("$msg [${Thread.currentThread().name}]")
}

/*
Coroutine Outer [main]
Coroutine A, 0 [main]
Coroutine A, 1 [main]
Coroutine A, 2 [main]
Coroutine A, 3 [main]
Coroutine A, 4 [main]
Coroutine B, 0 [main]
Coroutine B, 1 [main]
Coroutine B, 2 [main]
Coroutine B, 3 [main]
Coroutine B, 4 [main]*/
  • 위에서는 총 3개의 코루틴이 실행되고 있음(runBlocking과 launch2개)
  • runBlocking 시작->A를 5번 찍는 코루틴 만남->B 5번 찍는 코루틴 만나면 끝남
  • 이때 코루틴 안에 있는 모든 function들이 suspend function이 아니기 때문에 중간에 코루틴을 나가는 일 없이 A,B찍는게 순차적으로 실행되고 당연히 모두 main 쓰레드에서 실행됨

fun main() = runBlocking { // this: CoroutineScope
    launch {
        repeat(5) {
            println("Coroutine A, $it")
            delay(10L)
        }
    }
    launch {
        repeat(5) {
            println("Coroutine B, $it")
        }
    }
    println("Coroutine Outer")
}

/*Coroutine Outer [main]
Coroutine A, 0 [main]
Coroutine B, 0 [main]
Coroutine B, 1 [main]
Coroutine B, 2 [main]
Coroutine B, 3 [main]
Coroutine B, 4 [main]
Coroutine A, 1 [main]
Coroutine A, 2 [main]
Coroutine A, 3 [main]
Coroutine A, 4 [main]*/
  • runBlocking 코루틴 실행->A코루틴 실행되어 A 출력->suspend function인 delay만나서 코루틴 탈출->delay실행되는 동안 B 코루틴 실행->B코루틴 안에는 코루틴을 탈출시키는 suspend function이 없기 때문에 B출력 5번 모두 끝냄->suspend function처리 끝내고 다시 A코루틴으로 진입해서 뭠췄던 반복 다시 실행

fun main() = runBlocking { // this: CoroutineScope
    launch {
        repeat(5) {
            println("Coroutine A, $it")
            delay(10L)
        }
    }
    launch {
        repeat(5) {
            println("Coroutine B, $it")
            delay(10L)
        }
    }
    println("Coroutine Outer")
}

/*Coroutine Outer [main]
Coroutine A, 0 [main]
Coroutine B, 0 [main]
Coroutine A, 1 [main]
Coroutine B, 1 [main]
Coroutine A, 2 [main]
Coroutine B, 2 [main]
Coroutine A, 3 [main]
Coroutine B, 3 [main]
Coroutine A, 4 [main]
Coroutine B, 4 [main]*/
  • 위에서는 왜 A,B가 순차적으로 찍힐까? 일단 runBlocking 코루틴 실행->A 코루틴 만나서 A찍었는데 suspend function인 delay를 만나서 코루틴을 잠시 탈출 후 delay 실행->이때 runBlocking의 다른 코루틴인 B코루틴 실행되어 B찍음->suspend function인 delay를 만나서 코루틴을 잠시 탈출 후 delay 실행->delay를 마치고 다시 A코루틴으로 진입되어 잠시 멈춰졌던 다음 반복실행->delay만남......
  • 이때, Coroutine Outer가 제일 먼저 출력되는 이유? 일단 launch로 코루틴 실행되기 위한 스케쥴링 시켜놓는거라서 Coroutine Outer가 먼저 출력됨

✅Cacellation and Timeouts

  • Job
    • cancle()
  • Cancellation is cooperative
    • way1 : suspend를 주기적으로 호출
    • way2 : isActive로 상태 체크
  • Timeout
    • withTimeout
    • withTimeoutOrNull

✔ Cacellation and Timeouts

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping ${i++} ...")
            delay(500L)
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancel()// cancels the job and waits for its completion
    job.join()
    //위 두 함수를 동시에 실행하는  job.cancelAndJoin() 으로 해도 됨
    println("main: Now I can quit.")
}
  • 코루틴을 잘 취소시키는 건 중요하다. 메모리 리소스를 잡아먹기 때문이다
  • 우선 runBlocking 으로 메인 스레드는 블로킹 시킨다. 그리고 launch를 이용해서 코루틴을 하나 만든다. 코루틴은 1000번 반복하면서 숫자를 0.5초 간격으로 출력한다. 1.3초정도 지난다음에, 앱이 끝났으면 좋겠다면 cancel()을 호출하면 코루틴을 취소 가능하다.
  • 여기서, 주의할 점이 있다. 코루틴 내부에서 cancle()을 호출해도 코루틴이 계속 동작한다. 그 이유는, 코루틴이 취소되려면 코루틴 코드 자체에서 cancel을 체크해야 한다. 즉 코루틴 스스로가 취소에 협조적이어야 한다.
  • 그렇다면 cancle가능한 코드=코루틴이 스스로가 취소에 협조적인 코드는 어떻게 만드는가?
    • 방법1) 주기적으로 suspend function을 호출. 호출되었다가 다시 재개될 때 cancle되었는지 확인해서 exception을 던져주는 방식
    • 방법2) 명시적으로 상태를 체크해서 상태가 isActive 상태가 아니면 코루틴을 종료시키는 방식

~방법 1: 주기적으로 suspend function을 호출~
fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        try {
            var nextPrintTime = startTime
            var i = 0
            while (i < 5) { // computation loop, just wastes CPU
                // print a message twice a second
                if (System.currentTimeMillis() >= nextPrintTime) {
                    // delay(1L)
                    yield() //suspend 함수를 하나라도 실행해야함!!
                    println("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 500L
                }
            }
        }catch (e:Exception){
            println("Exception: $e")
        }
    }

    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.")
}

/*
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
Exception: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@4f7b794d
main: Now I can quit.*/
  • 코루틴 내에서 suspend가 되었다가 다시 재개될 때, 재개될 시점에 suspend function 이 exception을 던진자. exception을 체크해보려면 zhfnxls sodptj try-catch문으로 확인해볼 수 있다
  • 위 코드에서 delay로 exception을 던져서 코루틴을 취소할 수도있지만 이런 로직을 위해 코루틴가이드에서 제공하는 function이 yield() 이다

~방법2: isActive 상태가 아니면 코루틴을 종료~
fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // cancellable computation loop
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: 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.")
}

/*job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.*/
  • 위 로직은 exception을 던져서 job을 종료시키는게 아니라 무한 반복문인 while문 안의 조건으로 isActive를 주면 isActive가 상태를 체크해가며 cancle되어야할때 코루틴을 cancle시킴

✔ Closing resources with finally

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
        //코루틴이 예외를 던지면 finally블록 안 로직을 실행하고 종료된다
        //따라서 리소스를 해제하는 로직은 여기서 작성한다
            println("job: I'm running finally")
        }
    }
    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.")
}
  • 코루틴을 종료할 때 리소스를 어떻게 해제할 수 있는지 알아보자. 코루틴을 종료할 때, 코루틴에서 네트워크를 쓰거나 DB를 쓰다가 코루틴이 cancel되면, 리소스를 close해주어야 한다.
  • 리소스를 해제할 위치는 suspend 함수가 exception 발생시키는 위치에서 finally 블록에서 해제해주면 된다.

✔ Run non-cancelaanle bloc

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                println("job: I'm running finally")
                delay(1000L)
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    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.")
}
  • cancel된 코루틴 안에서 다시 또 코루틴을 실행해야 하는 rare한 케이스
  • withContext 이라는 코루틴 스코프를 사용하면 되고 이럴일 별로 없긴 한데 알아두자..

✔ withTimeout

fun main() = runBlocking {
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
}
  • timeout은 코루틴의 job을 가지고 cancel 하는 것이 아니고, 코루틴을 실행할 때, 해당 시간이 지나면 이 코루틴은 취소된다고 미리 timeout을 지정하는 방식
  • 아래의 예시는 runBlocking 안에서 실행하고 있어서 TimeoutCancellationException이 발생

✔ withTimeoutOrNull

fun main() = runBlocking {
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done" // will get cancelled before it produces this result
    }
    println("Result is $result")
}
  • 위의 exception 던지는 것을 해결하기 위해서는 아래와 같이 withTimeoutOrNull을 활용할 수 있다.

✅Composing Suspending Functions

  • Async to sequential
    • Sequential by default
    • The Dream Code on Android
  • async
    • Concurrent using async
    • Lazily started async
  • Structred concurrency
    • Async-style function
    • Structred concurrency with async

✔ Sequential by default

fun main() = runBlocking {
   val time = measureTimeMillis {
       val one = doSomethingOne()
       val two = doSomethingTwo()
       println("The answer is ${one + two}")
   }
   println("Completed in $time ms")
}

suspend fun doSomethingOne(): Int {
   println("doSomethingOne")
   delay(1000L)
   //서버통신같은 heavy한 코드 즉, 비동기로 처리되어야하는 동작!
   return 13
}

suspend fun doSomethingTwo(): Int {
   println("doSomethingTwo")
   delay(1000L)
   //서버통신같은 heavy한 코드 즉, 비동기로 처리되어야하는 동작!
   return 29
}

/*doSomethingOne
doSomethingTwo
The answer is 42
Completed in 2013 ms*/
  • suspend function 을 어떻게 조합해서 코루틴을 유용하게 사용할 수 있을까
    retrofit 호출 같은 것(=heavy 한 비동기 job)을 순차적으로 실행하고 싶으면 어떻게 해야되는가?

  • 코루틴에서는 일반 코드처럼 작성하면, 비동기일지라도 순차적으로 실행되는 것이 기본이다.

  • 즉 비동기 실행을 순차적으로 맞춰줄 수 있고, 콜백을 순차 실행한다.(와우..코루틴 없이 콜백 지옥으로 retrofit처리했을때는 끝나는 순서가 제각각이라 별도의 체크가 필요했는데..!)

  • 위의 예시에서 function 2개가 거의 동시에 실행되어서 콜백을 받을때 어느게 먼저 끝날 지 몰라서 비동기 처리하기 어려웠었음 근데 코루틴을 알아서 비동기처리가 순차실행되니까 One끝나고 Two실행됨

  • 이건 안드로이드에서 말하는 dream Code의 시작점이 되는건데, 일반 코드 적듯 순차적으로 비동기 처리를 적기만 하면 알아서 순차처리되어서 싱크도 딱딱 맞는다

    ✔ The Dream Code on Android

 class MainActivity : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?){
    	super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initUi()
        
        // case1
        button_coroutines.setOnClickListener {
        	CoroutineScope(Dispatchers.Main).launch { // 메인 스레드를 블로킹하지 않음
            	runDreamCode() // Ui 업데이트 됨
            }
        }
        
        // case2
        button_blocking.setOnClickListener {
        	runBlocking { // 메인 스레드를 블로킹한다.
            	runDreamCode() // Ui 업데이트 안됨
            }
        }
    }

suspend fun runDreamCode() {
    val time = measureTimeMillis {
    	val one = doSomethingUsefuleOne() // CPU를 많이 먹는 동작1
    	val two = doSomethingUsefuleTwo() // CPU를 많이 먹는 동작2
    	println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefuleOne(): Int {
	println("doSomethingUsefuleOne")
	delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
	delay(1000L)
    return 29
 }
}
  • case1 ) 코루틴 안에서 일반 코드처럼 작성하였고, 심지어 메인 스레드에서 실행되었지만, UI를 블로킹하지 않는다. UI는 UI대로 그려지고 있고 코루틴 안에서 일어나는 일들은 다른 스레드에서 실행되는 것처럼 순차적으로 실행된다.

  • case2 ) 코루틴이 아닌 형태로, runBlocking을 활용하여 명시적으로 메인 스레드를 블로킹하도록 한다면 어떻게 될까? -> 모든 UI가 다 멈춘다.

    ✔ Concurrent using async

 fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}
  • 만약 첫번째와 두번째 연산이 dependency가 없다면=독립적으로 일어나도 되는 일이라면, 굳이 순차적으로 실행하지 않고, 비동기적으로 실행하고 싶을 것이다

  • 이럴경우 항상 async 이용해서 명시적으로 콜해야한다

  • 아까 코드는 one을 실행하고 one이 suspend니까 기다릴 후 two가 실행되었다면 이번 코드는 async코드블럭 실행후 안기다리고 바로 다음 async를 실행한다

  • await()는 서로의 결과를 기다린 후 더해야하니까 사용한다

  • 이런식으로 사용하면 비동기를 순차실행, 동시실행 등 자유자재로 코루틴을 사용할 수 있다.

  • async 는 Job을 상속한 객체를 반환한다.Job을 상속받았으니까 await(),cancle()같은 function도 사용할 수 있는거고, one.await() 하면 job이 끝날때까지 기다린다.

    ✔ Lazily started async

 fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        // some computation
        one.start() // start the first one
        two.start() // start the second one
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}
  • async 자체는 코루틴 빌더인데, 바로 시작하지 않고 start()를 호출해서 시작 시점을 명시하고 싶다면, 여기에 start옵션으로 LAZY를 걸 수도 있다.
  • LAZY를 걸지 않은 앞선 코드는 바로 async코드가 실행됐었다 LAZY를 걸면 start()를 걸 때 async가 시작된다 start()가 없었다면 await()를 만났을때 실행된다.
  • 즉, suspend함수를 async로 만들어서 동시 처리 시키든, LAZY같은거 걸어서 나중에 start() 시키든 원하는대로 사용가능 하다!

    ✔ Async-style function

  / note that we don't have `runBlocking` to the right of `main` in this example
fun main() {
    val time = measureTimeMillis {
        // we can initiate async actions outside of a coroutine
        val one = somethingUsefulOneAsync()
        val two = somethingUsefulTwoAsync()
        // but waiting for a result must involve either suspending or blocking.
        // here we use `runBlocking { ... }` to block the main thread while waiting for the result
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")
}

@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}
  • 다음과 같이 GlobalScope에서 독립적으로 코루틴을 실행시키는 코드는 짜면 안된다. GlobalScope에서 launch를 사용하면 exception이 났을 때 돌이킬 수 없는 상황이 발생할 수 있다.
  • GlobalScope에서 실행되는 로직들은 애플리케이션 전체 영역에서 실행될 수 있기 때문에 지역적으로 일어나는 exception같은거랑 관계가 없다
  • 여기서, somethingUsefulOneAsync, somethingUsefulTwoAsync 함수는 일반 함수라서 어디서든 사용 가능하다. coroutine scope 안에 포함되어있지 않기 때문에 GlobalScope에서 실행된 후 발생할 수 있는 exception과 전혀 관계가 없게된다(somethingUsefulOneAsync()이랑 GlobalScope에서 실행되는 로직은 dependency가 있는데도 불구하고)
  • 이런 상황은 structured concurrency 를 사용해서 해결할 수 있다.

✔ Structred concurrency with async


fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        println("The answer is ${concurrentSum()}")
    }
    println("Completed in $time ms")
}

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}
  • coroutine scope 로 한번 감싸서, 어디서 쓸 수 있는 형태가 아닌, coroutine scope 안에서만 사용할 수 있도록 바꾼다. 코루틴들 사이에서 exception 이 발생되면, exception이 전파되면서 코루틴이 전부 취소된다.
  fun main() = runBlocking<Unit> {
    try {
        failedConcurrentSum()
    } catch(e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}

suspend fun failedConcurrentSum(): Int = coroutineScope {
    val one = async<Int> { 
        try {
            delay(Long.MAX_VALUE) // Emulates very long computation
            42
        } finally {
            println("First child was cancelled")
        }
    }
    val two = async<Int> { 
        println("Second child throws an exception")
        throw ArithmeticException()
    }
    one.await() + two.await()
}
  • 어떤 코루틴에서 exception이 발생했을 때, 코루틴이 cancel되면, hierarchy로 전파가 된다. 전파가 되면 다 종료가 된다.

  • coroutineScope 안에서 async 코루틴 빌더가 2개 동시적으로 실행되고 있고 어떤 async 블록에서 ArithmeticException 를 던지면 해당 async가 포함된 coroutineScope를 불러낸 부모 코루틴인 runBlocking에 취소가 전파되어서 예외를 catch하게됨

  • 이런 실행 취소 전파가 의미하는건 실행 중 연관되어있던 코루틴 중 예외가 발생하면 다른 코루틴 모두 예외를 전파받고, 정상적으로 리소스를 뱉어낸 후 종료죌 수 있다는 것

    ✅Coroutines under the hood

  • 어떻게 내가 순차적으로 작성한 코드가 비동기도 되고 콜백도 되고 하는거지?
    어떻게 코루틴에서 중단되었다가 재개될 수 있는거지?
    코틀린이 내부적으로 어떻게 동작하는 걸까? 마법은 없다..

  • Continuation-Passing Style = CPS
    내부적으로, 콜백같은걸 계속 넘겨주면서 콜백 콜백 되는거다..
    kotlin suspending function

  • CPS transformation
    JVM에 들어갈 때는, 우리가 호출한 함수에 Continuation cont 가 인수에 생긴다.

  • 코루틴 스코프에서 모든 코루틴들을 실행하도록 하고, 화면을 나가면, 코루틴 스코프에 cancel을 해주면 모든 job들이 취소가 된다.
    안드로이드에서, 라이프사이클이 있는 클래스인 경우, 라이프사이클에 맞게 코루틴 스코프를 연결해놓고 실행시키면 된다.

    ✅Coroutine Context and Dispatchers

    ✔ Dispatcher에 따른 스레드 지정

fun main() = runBlocking<Unit> {
    launch { // context of the parent, main runBlocking coroutine
        println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    newSingleThreadContext("MyOwnThread").use { // will get its own new thread
        launch(it) {
            println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
        }
    }
  • 코루틴 컨텍스트 요소에는 dispatcher가 있다. dispatcher는 코루틴이 어떤 스레드나 스레드풀에서 실행될지 결정하는 element이다. 코루틴 빌더는 모두 옵션으로 coroutine context를 받는다.
  • launch: runBlocking에서 옵셔널 파라미터 없이 그냥 실행하면, 자신이 실행된 컨텍스트 상속받아서 작업하기 때문에, 메인 스레드(=runBlocking 이랑 같은 컨텍스트)
  • launch(Dispatchers.Unconfined): 메인 스레드에서 실행.
  • launch(Dispatchers.Default) : DefaultDispatcher-worker-1에서 실행. 글로벌 스코프에서 실행했던 그런 코루틴들이 실행되는 스레드(= 기본 스레드)와 같은 스레드.
  • newSingleThreadContext : 코루틴 실행할 때마다 스레드 하나 만든다.

    ✔ 스레드를 스위칭하기

  fun main() {
    newSingleThreadContext("Ctx1").use { ctx1 ->
        newSingleThreadContext("Ctx2").use { ctx2 ->
            runBlocking(ctx1) {
                log("Started in ctx1")
                withContext(ctx2) {
                    log("Working in ctx2")
                }
                log("Back to ctx1")
            }
        }
    }
}
  • withContext를 사용하면 스레드를 점프해서 스위칭할 수 있다. 스레드 context를 만들었을 때, use 를 이용해서 스레드를 close를 해주었다
  • 즉 같은 코루틴 하나가 스레드1->2->1 로 스위칭해서 돌아온다

✔ 부모 코루틴과 자식 코루틴, GlobalScope

fun main() = runBlocking<Unit> {
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        // it spawns two other jobs
        GlobalScope.launch(Job()) {
            println("job1: I run in my own Job and execute independently!")
            delay(1000)
            println("job1: I am not affected by cancellation of the request")
        }
        // and the other inherits the parent context
        launch {
            delay(100)
            println("job2: I am a child of the request coroutine")
            delay(1000)
            println("job2: I will not execute this line if my parent request is cancelled")
        }
    }
    delay(500)
    request.cancel() // cancel processing of the request
    delay(1000) // delay a second to see what happens
    println("main: Who has survived request cancellation?")
}

/*
job1: I run in my own Job and execute independently!
job2: I am a child of the request coroutine
job1: I am not affected by cancellation of the request
main: Who has survived request cancellation?*/
  • 어떤 새로운 코루틴이 실행되면, 부모 코루틴의 자식이 된다.
  • GlobalScope는 독립적으로 job이 생성되고 부모-자식 관계가 생성되지 않는다. - 부모 코루틴이 cancel 되면 자식 코루틴도 cancel 되는가? -> 당연히 자식 코루틴만 종료가 된다. GlobalScope는 종료되지 않는다.

✔ 부모 코루틴은 자식 코루틴을 기다려준다

fun main() = runBlocking<Unit> {
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        repeat(3) { i -> // launch a few children jobs
            launch  {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, 600ms
                println("Coroutine $i is done")
            }
        }
        println("request: I'm done and I don't explicitly join my children that are still active")
    }
    println("Now processing of the request is complete")
}
/*Now processing of the request is complete
request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done*/
  • 부모-자식 관계 코루틴으로 만들지 않으면 아래처럼 안기다려줌
fun main() = runBlocking<Unit> {
    // launch a coroutine to process some kind of incoming request
    val request = CoroutineScope(Dispatchers.Default).launch {
        repeat(3) { i -> // launch a few children jobs
            launch {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, 600ms
                println("Coroutine $i is done")
            }
        }
        println("request: I'm done and I don't explicitly join my children that are still active")
    }
    println("Now processing of the request is complete")
}
//Now processing of the request is complete

✔ 라이프사이클이 있는 클래스에서 코루틴 사용할 때, 라이프사이클에 맞게 코루틴 스코프 지정하기

class Activity {
    private val mainScope = CoroutineScope(Dispatchers.Default) // use Default for test purposes
    
    fun destroy() {
        mainScope.cancel()
    }

    fun doSomething() {
        // launch ten coroutines for a demo, each working for a different time
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
                println("Coroutine $i is done")
            }
        }
    }
} // class Activity ends

fun main() = runBlocking<Unit> {
    val activity = Activity()
    activity.doSomething() // run test function
    println("Launched coroutines")
    delay(500L) // delay for half a second
    println("Destroying activity!")
    activity.destroy() // cancels all coroutines
    delay(1000) // visually confirm that they don't work
}
  • 코루틴 스코프에서 모든 코루틴들을 실행하도록 하고, 화면을 나가면(destroy), 코루틴 스코프에 cancel을 해주면 mainScope의 모든 job들이 취소가 된다. 혹은 안드로이드에서, 라이프사이클이 있는 클래스인 경우, 라이프사이클에 맞게 코루틴 스코프를 연결해놓고 실행시키면 된다.
  • activity.destroy()를 호출하지 않으면 activity가 끝났는데도 코루틴이 계속 실행되고 있다

코루틴 깊게 알아보자..

profile
'왜?'라는 물음을 해결하며 마지막 개념까지 공부합니다✍

0개의 댓글

관련 채용 정보