ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android] Kotlin Coroutines 1주차
    Android 2021. 7. 28. 17:14
    반응형

    Asynchronous programming techniques

    사용자가 사용할 때 기다리거나 병목현상이 발생하는 것을 피하고 싶습니다.

    위와 같은 문제를 해결하기 위해서 다음과 같은 방식이 있습니다.

    Threading

    제일 보편적인 방식입니다.

    fun postItem(item: Item) {
        val token = preparePost()
        val post = submitPost(token, item)
        processPost(post)
    }
    
    fun preparePost(): Token {
        // makes a request and consequently blocks the main thread
        return token
    }

    함수가 UI를 차단한다고 한다면 별도의 스레드를 실행해 UI가 차단되는 것을 막을 수 있습니다.

    하지만 다음과 같은 단점이 있습니다.

    1. 비용이 많이 듭니다.
    2. 개수가 무제한이 아닙니다.
    3. 지원하지 않는 플랫폼도 있습니다.
    4. 사용하는 것이 쉽지 않습니다.

    Callbacks

    콜백은 하나의 함수를 다른 함수의 매개변수로 전달하고, 하나가 완료되면 다른 하나를 호출합니다.

    fun postItem(item: Item) {
        val token = preparePost()
        val post = submitPost(token, item)
        processPost(post)
    }
    
    fun preparePost(): Token {
        // makes a request and consequently blocks the main thread
        return token
    }

    콜백에는 다음과 같은 단점이 있습니다.

    1. 콜백이 중첩될 수 있습니다.
    2. 중첩이 되면 오류 처리가 복잡해집니다.

    Futures, promises

    Futures는 미래에 실행이 완료될 것으로 예상되는 객체를 의미한다.

    아직 실행되지 않은 경우나, 실행 중이지만 아직 완료되지 않는 경우를 포함한다.

    Futres는 외부에서 완료된 결과가 들어오기만 하므로 read-only 지만 Promises는 강제로 complete 할 수 있다는 차이점이 있다.

    fun postItem(item: Item) {
        preparePostAsync()
            .thenCompose { token ->
                submitPostAsync(token, item)
            }
            .thenAccept { post ->
                processPost(post)
            }
    
    }
    
    fun preparePostAsync(): Promise<Token> {
        // makes request and returns a promise that is completed later
        return promise
    }

    Reactive Extensions

    모든것은 stream이고 관찰이 가능하다는 것을 바탕으로 합니다.

    옵서버 패턴을 사용합니다.

    Coroutines

    Kotlin의 비동기 작업을 수행합니다.

    함수를 일시 중단하고 나중에 다시 시작할 수 있는 개념입니다.

    fun postItem(item: Item) {
        launch {
            val token = preparePost()
            val post = submitPost(token, item)
            processPost(post)
        }
    }
    
    suspend fun preparePost(): Token {
        // makes a request and suspends the coroutine
        return suspendCoroutine { /* ... */ }
    }

    일시 중단 함수라는 뜻으로 suspend 키워드를 사용합니다.

    코루틴은 다음과 같은 특징을 가집니다.

    1. 함수는 동일하게 유지되고 차이점은 suspend가 추가된다는 것입니다.
    2. 코루틴을 시작하는 launch 함수 외에 동기식 코드와 동일하게 작성됩니다.
    3. 루프나 예외 처리등 계속 사용할 수 있습니다.
    4. 플랫폼에 독립적입니다.

     

    Coroutines basics

    코루틴은 일시 중단이 가능한 인스턴스입니다.

    스레드와 유사하지만 코루틴은 특정 스레드에 구속되지 않고 한 스레드에서 일시 중단하고
    다른 스레드에서 다시 시작할 수 있습니다.

    fun main() = runBlocking { // CoroutineScope
        launch { // 새로운 코루틴을 실행
            delay(1000L) // 1초 비차단 지연
            println("World!") // delay 후 출력
        }
        println("Hello") // 이전 코루틴이 지연되는 동안 메인 코루틴은 진행됩니다.
    }
    
    // 출력
    "Hello"
    "World!"
    • launch → 새 코루틴을 시작하는 코루틴 빌더입니다.
    • delay → 특정 시간 동안 코루틴을 일시 중지 시키는 함수입니다. 스레드가 차단되지 않고 다른 코루틴을 실행합니다.
    • runBlocking → fun main()의 비 코루틴 영역과 runBlocking 내부의 코루틴이 있는 코드를 연결하는 코루틴 빌더입니다. runBlocking 내부의 코루틴들이 완료될 때까지 이를 실행하는 스레드를 차단합니다.

    코루틴은 동시성의 원칙을 따릅니다.

    즉, 새로운 코루틴은 코루틴의 수명을 제한하는 특정 CoroutineScope에서만 시작될 수 있습니다.

     

    suspend 키워드를 함수 앞에 추가하면 일시 중단 함수가 됩니다.

    fun main() = runBlocking { // this: CoroutineScope
        launch { doWorld() }
        println("Hello")
    }
    
    // this is your first suspending function
    suspend fun doWorld() {
        delay(1000L)
        println("World!")
    }

    runBlocking, coroutineScope 차이점

    • runBlocking : 자식 스레드가 완료될 때까지 현재 스레드를 block 한다. (일반 함수)
    • coroutineScope : 자식 스레드가 완료될 때 까지 현재 스레드를 block 하지 않는다. (일시 중단 함수)
    fun main() = runBlocking { // this: CoroutineScope
        launch { 
            delay(200L)
            println("Task from runBlocking")
        }
    
        coroutineScope { // Creates a new coroutine scope
            launch {
                delay(900L) 
                println("Task from nested launch")
            }
    
            delay(100L)
            println("Task from coroutine scope") // This line will be printed before nested launch
        }
    
        println("Coroutine scope is over") // This line is not printed until nested launch completes
    
    //
    Task from coroutine scope
    Task from runBlocking
    Task from nested launch
    Coroutine scope is over

     

    Job은 코루틴에 대한 객체를 반환받아 제어할 수 있습니다.

    val job = launch { // launch a new coroutine and keep a reference to its Job
        delay(1000L)
        println("World!")
    }
    println("Hello")
    job.join() // wait until child coroutine completes
    println("Done") 
    
    // Hello
    // World!
    // Done

     

    Intro to coroutines and channels - hands on tutorial

    네트워크 통신 시 스레드를 차단하지 않고 코루틴을 일시 중단합니다.

    block -> suspend
    thread -> coroutine

    코루틴이 일시 중지되면 작업 중이던 계산이 일시 중지되고 스레드에서 된 다음 메모리에 저장됩니다.

    다시 시작이 되면 스레드로 반환합니다.

    async는 새로운 코루틴을 시작하고 결과 반환 받을 수 있는 Deferred를 반환합니다.

    Deferred는 Job을 확장하기 때문에 상태 제어도 가능합니다.

    결과를 얻으려면 await() 함수를 호출합니다.

    launch는 새로운 코루틴을 시작하지만 결과를 반환받지 못하고 Job을 반환합니다.

    fun main() = runBlocking {
        val deferred: Deferred<Int> = async(Dispatchers.Default) {
            loadData()
        }
        println("waiting...")
        println(deferred.await())
    }
    
    suspend fun loadData(): Int {
        println("loading...")
        delay(1000L)
        println("loaded!")
        return 42
    }
    //main 시작 -> loadData 시작 -> waiting 출력 -> main 일시정지 -> loading 출력 
    // -> loadData 1초 일시정지 -> loadData 다시시작 -> loaded 출력 -> 42 반환 -> loadData 종료
    // -> main 다시시작 -> 42 출력
    
    
    // waiting...
    // loading...
    // loaded!
    // 42

    runBlocking은 regular과 suspend, blocking과 non-blocking 사이의 다리 역할을 합니다.

    async를 사용할 때 flatMap을 사용할 필요가 없고 map은 list of list가 아니라 list of Deferred입니다.

    awaitAll()은 List <List <any>>를 반환하기 때문에 결과를 얻으려면 flatten(). aggregate()를 호출하면 됩니다.

    CoroutineDispatcher는 코 루틴이 실행되어야 하는 스레드를 결정합니다.

    만약 지정하지 않으면 외부 scope의 dispatcher를 사용합니다.

    • Dispatchers.Default : CPU 사용량이 많은 작업에 사용합니다.
    • Dispatchers.IO : 네트워크나 로컬 DB 등 통신을 할 때 주로 사용합니다.
    • Dispatchers.Main : Main 스레드를 사용합니다. ( UI 스레드)

    withContext는 coroutine context를 지정하여 주어진 코드를 호출하고 완료될 때까지 일시 중단하고 결과를 반환합니다.

     

    Cancellation and timeouts

    Cancelling coroutine execution

    launch는 실행 중인 코루틴을 취소하는 데 사용하는 Job을 반환합니다.

    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
    job.join() // waits for job's 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.

    Cancellation is cooperative

    모든 코루틴은 취소되면, CancellationException을 발생시킵니다.

    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // computation loop, just wastes CPU
            // 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!
    // job: I'm sleeping 3 ...
    // job: I'm sleeping 4 ...
    // main: Now I can quit.
    • join : 현재의 coroutine 동작이 끝날 때까지 대기한다.
    • cancel : 현재 coroutine을 즉시 종료하도록 유도만 하고 대기하지 않는다. 다만 타이트하게 동작하는 단순 루프에서는 delay가 없다면 종료하지 못한다.
    • cancelAndJoin : 현재 coroutine에 종료하라는 신호를 보내고, 정상 종료할 때까지 대기한다.

    Making computation code cancellable

    위와 같은 computation 코드를 보면 cancel을 시켰음에도 불구하고 반복문을 전부 돌고 나서야 종료되는 것을 확인할 수 있습니다.

    이러한 computation 코드를 cancel 시키는 방법은 두 가지가 있습니다.

    • 명시적으로 cancel 상태를 체크한다. (isActive 이용)
    • yield function을 이용하여 추기적으로 cancel을 체크하는 suspend function을 invoke 시킨다.

    isActive는 CoroutineScope 객체를 통해 코루틴 내부에서 사용할 수 있는 확장 속성입니다.

    Closing resources with finally

    coroutine을 cancel한다음, 마지막으로 해야 할 작업이 필요할 때는 try - finally 구문을 이용합니다.

    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } 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.")
    
    // job: I'm sleeping 0 ...
    // job: I'm sleeping 1 ...
    // job: I'm sleeping 2 ...
    // main: I'm tired of waiting!
    // job: I'm running finally
    // main: Now I can quit.

    Run non-cancellable block

    만약 finally 내부에 다시 suspending function을 사용한다면 cancellationException이 발생합니다.

    특이 케이스로 finally에서 일시중지 함수를 사용해야 한다면 withContext와 NonCancellable로 감싸줘야 합니다.

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

    Timeout

    코루틴을 하나하나 Job으로 cencel하지 않고, 특정 시간 이후에 cencel 되도록 하려면 withTimeout을 사용하면 됩니다.

    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    
    // I'm sleeping 0 ...
    // I'm sleeping 1 ...
    // I'm sleeping 2 ...
    // Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

     

    TimeoutCancellationException은 CancellationException의 하위 클래스입니다.

    timeout 처리가 필요하다면 try {...} catch 문을 사용해야 되지만 withTimeoutOrNull을 사용하면 exception throw 대신 null을 return 합니다.

    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")
    
    // I'm sleeping 0 ...
    // I'm sleeping 1 ...
    // I'm sleeping 2 ...
    // Result is null

    Composing suspending functions

    Sequential by default

    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
    }

    순차적으로 코드를 실행할 두 개의 일시 중지 함수가 있다고 가정합니다.

    두 개의 작업을 하는데 시간이 필요하므로 1초의 딜레이로 표현합니다.

    val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
    
    // The answer is 42
    // Completed in 2017 ms

    Concurrent using async

    이전 코드에서 의존성이 없고 두 함수를 동시에 수행하여 더 빠른 시간에 종료시키고 싶습니다.

    async 함수를 사용하여 두 개의 작업을 동시에 비동기적으로 처리할 수 있습니다.

    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
    
    // The answer is 42
    // Completed in 1017 ms

    두 개의 함수는 동시에 실행되기 때문에 약 두배 가까이 빨라집니다.

    async로 시작되는 코 루틴은 블록 내 코드의 수행을 지연시킬 수 있습니다.

    매개변수인 start 값을 CoroutineStart.LAZY를 설정하면 코루틴이 지연됩니다.

    작업을 수행하려면 start()를 호출하거나 await()를 호출하면 됩니다.

    val time = measureTimeMillis { 
    val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() } 
    val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() } 
    
    one.start() 
    two.start() 
    println("The answer is ${one.await() + two.await()}") 
    } 
    println("Completed in $time ms")

    Async-style functions

    // The result type of somethingUsefulOneAsync is Deferred<Int>
    @OptIn(DelicateCoroutinesApi::class)
    fun somethingUsefulOneAsync() = GlobalScope.async {
        doSomethingUsefulOne()
    }
    
    // The result type of somethingUsefulTwoAsync is Deferred<Int>
    @OptIn(DelicateCoroutinesApi::class)
    fun somethingUsefulTwoAsync() = GlobalScope.async {
        doSomethingUsefulTwo()
    }

    GlobalScope에서 async coroutine builder을 통해 명시적으로 비동기 함수를 만들 수 있습니다.

    이 함수는 일시 중지 함수는 아닙니다.

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

    이와 같은 사용은 kotlin에서는 다음과 같은 이유로 강력하게 비추천합니다.

    • val one = somthingUsefullOneAsync()과 one.await() 사이에서 exception이 발생하면 프로그램이 종료됩니다.
    • 외부의 try-catch로 처리하여 exception handling은 할 수 있으나 비동기 작업이 유지된 채로 남습니다.

    Structured concurrency with async

    fun main(){
    	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()
    }

    async coroutine builder는 CotourineScope의 확장 함수입니다.

    위의 코드처럼 새로운 coroutineScope 안에서 async를 실행한다면, 두 개의 aysnc는 같은 scope에서 수행하는 작업들이 됩니다.

    scope 안에서 exception이 발생하면 해당 scope을 빠져나가면서 해당 coroutineScope에서 수행되었던 자식 coroutine들도 다 취소됩니다. 그렇게 되면 프로그램이 종료되는 일이 발생하지 않습니다.

    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()
    }
    
    // Second child throws an exception
    // First child was cancelled
    // Computation failed with ArithmeticException

    cancel은 항상 코루틴 계층 구조를 통해 전파됩니다.

    그렇기 때문에 위의 코드는 two 변수에서 exception을 발생시키면 one 변수와 main 함수로 exception이 전파됩니다.

    Coroutine context and dispatchers

    Dispatchers and threads

    coroutine context는 어떤 스레드에서 코루틴을 실행할지에 대한 coroutine dispatcher를 가지고 있습니다.

    coroutine dispatcher는 코루틴을 실행시킬 스레드를 지정하거나 스레드 풀을 지정하거나, 지정하지 않고 실행되도록 할 수 있습니다.

    launch와 async 같은 coroutine builder들은 명시적으로 dispatcher를 지정할 수 있는 CoroutineContext 매개변수를 선택할 수 있습니다.

    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}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }
    
    // Unconfined            : I'm working in thread main
    // Default               : I'm working in thread DefaultDispatcher-worker-1
    // newSingleThreadContext: I'm working in thread MyOwnThread
    // main runBlocking      : I'm working in thread main
    • 파라미터 없이 launch를 정의하면 부모 CoroutineScope의 context와 dispatcher를 상속받습니다.
    • Dispatchers.Default는 명시적으로 지정된 dispatchers가 없을 때 사용되는 기본입니다. GlobalScope.launch()와 동일합니다. 공유해서 사용하는 스레드 풀을 이용합니다.
    • newSingleThreadContext는 코루틴을 실행할 스레드를 생성하지만 매우 비싼 리소스입니다. 사용이 끝나면 무조건 해제시켜야 하고, top level에 변수로 선언하여 재활용해서 사용해야 합니다.

    Unconfined vs confined dispatcher

    Unconfined는 해당 코루틴을 callar thread에서 시작합니다.

    일시 중지된 후 다시 시작되면 적절한 스레드에 재 할당되어 시작됩니다.

    따라서, CPU 시간을 소비하지 않고 특정 스레드에 제한된 공유 데이터를 업데이트하지 않는 코루틴에 적합합니다. ( UI 같은 작업은 실행하면 안 됨)

    dispatcher는 기본적으로 외부 CoroutineScope를 상속합니다.

    runBlocking의 default dispatcher는 invoker thread로 제한되므로, 해당 코루틴들이 FIFO scheduling 될 거라고 예상할 수 있습니다.

    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
        println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
        delay(500)
        println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
    }
    launch { // context of the parent, main runBlocking coroutine
        println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
        delay(1000)
        println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
    }
    
    // Unconfined      : I'm working in thread main
    // main runBlocking: I'm working in thread main
    // Unconfined      : After delay in thread kotlinx.coroutines.DefaultExecutor
    // main runBlocking: After delay in thread main
    1. Unconfined로 지정했기 때문에 runblocking이 시작된 main thead에서 수행합니다.
    2. 아무것도 지정하지 않았기 때문에 runblocking의 dispatcher를 사용합니다.
    3. 일시 중지된 후 다시 시작된 거이기 때문에 main이 아닌 다른 스레드에서 수행합니다.
    4. 아무것도 지정하지 않았기 때문에 runblocking의 dispatcher를 사용합니다.

    Debugging coroutines and threads

    coroutine debugger를 사용하여 다음과 같은 작업을 수행할 수 있습니다.

    • 각각의 코루틴의 상태를 확인합니다.
    • 실행 중인 코루틴과 일시 중지된 코루틴의 로컬 및 캡쳐된 변수의 값을 확인합니다.
    • 코루틴 생성 스택과 코루틴 내부 호출 스택을 확인합니다.
    • 각각의 코루틴과 해당 스택의 상태가 포함된 보고서를 가져옵니다.

    -Dkotlinx.coroutines.debug를 JVM option에 추가하면 coroutine에 대한 정보도 로그로 남길 수 있습니다.

    val a = async {
        log("I'm computing a piece of the answer")
        6
    }
    val b = async {
        log("I'm computing another piece of the answer")
        7
    }
    log("The answer is ${a.await() * b.await()}")
    
    // [main @coroutine#2] I'm computing a piece of the answer
    // [main @coroutine#3] I'm computing another piece of the answer
    // [main @coroutine#1] The answer is 42

    Job in the context

    Job은 context의 일부입니다. coroutineContext [Job]을 사용하여 Job을 꺼낼 수 있습니다.

    CoroutineScope의 isActive는 coroutineContext [Job]?. isActive == true와 같습니다.

    println("My job is ${coroutineContext[Job]}")
    
    // My job is "coroutine#1":BlockingCoroutine{Active}@5b80350b

    Children of a coroutine

    CoroutineScope에서 다른 코루틴을 launch 하면 CoroutineScope.coroutineContext에 의해 context를 상속하고 새로 생성되는 Job은 부모 coroutine Job의 자식이 됩니다.

    부모 코루틴이 cancel 되면 자식 코루틴들도 재귀적으로 전부 cancel 됩니다.

    1. 코루틴을 실행할 때 명시적으로 다른 scope를 지정하면 부모 scope의 Job을 상속하지 않습니다.
    2. 다른 Job이 새 코루틴에 대한 context로 전달되면 부모 scope의 Job을 재정의 합니다.
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        // it spawns two other jobs
        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?

    job1은 명시적으로 지정해줬기 때문에 request가 cancel 돼도 정상적으로 실행되고 종료됩니다.

    job2은 request 코루틴의 자식 Job이므로 request가 cancel 된 시점에 job2도 cancel 됩니다.

    Parental responsibilities

    부모 코루틴은 항상 자식 코루틴이 전부 완료될 때까지 기다립니다.

    // 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")
    }
    request.join() // wait for completion of the request, including all its children
    println("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
    // Now processing of the request is complete

    Naming coroutines for debugging

    코루틴에도 이름을 지정해 줄 수 있습니다.

    log("Started main coroutine")
    // run two background value computations
    val v1 = async(CoroutineName("v1coroutine")) {
        delay(500)
        log("Computing v1")
        252
    }
    val v2 = async(CoroutineName("v2coroutine")) {
        delay(1000)
        log("Computing v2")
        6
    }
    log("The answer for v1 / v2 = ${v1.await() / v2.await()}")
    
    // [main @main#1] Started main coroutine
    // [main @v1coroutine#2] Computing v1
    // [main @v2coroutine#3] Computing v2
    // [main @main#1] The answer for v1 / v2 = 42

    Combining context elements

    coroutine context는 여러 요소를 가질 수 있고 이를 위해서 + 연산자를 사용할 수 있습니다.

    launch(Dispatchers.Default + CoroutineName("test")) {
        println("I'm working in thread ${Thread.currentThread().name}")
    }

     

    스터디 후 정리 내용

    • Coroutine -> 일시 중단이 가능한 인스턴스
    • CoroutineScope -> Coroutine을 실행시킬 수 있는 블록(범위)
    • CoroutineContext -> 해당 CoroutineScope의 정보를 담고 있는 집합체
    • CoroutineDispatchers -> 해당 CoroutineScope를 어떤 스레드에서 실행시킬지에 대한 정보를 담고 있다.
    • Job -> CoroutineScope를 담고 있는 변수. 상태 값을 알고 있어서 담고 있는 CoroutineScope 제어가 가능하다.
    • launch -> 코루틴을 시작하는 코루틴 빌더, 결과 값을 반환하지 않고 Job을 반환한다.
    • async -> 코루틴을 시작하는 코루틴 빌더, 결과 값을 반환받을 수 있는 Deferred를 반환한다.

     

    반응형

    댓글

Designed by Tistory.