비동기 프로그래밍

·2021년 12월 25일
0
post-thumbnail

📌 비동기 프로그래밍


모던 어플리케이션을 프로그래밍할 우리는 리모트 호출, DB업데이트, 검색 등을 해야하는 일이 자주 발생한다. 이런 대부분의 동작을 즉시 수행되지 않는다. 프로그램의 효율성을 올리기 위해서, 위와 답은 동작들은 비동기로 실행하여 비차단방식을 사용한다. 코루틴은 정확히 이런 문제를 해결하기 위한 목적으로 탄생했다.

순차적으로 시작하기

Klaxon 라이브러리를 사용하여 특정 공항에 스케줄 지연이 있는지 확인하기 위해서 날씨 정보응 가지고 오는 프로그램을 만들자.

Airport

class Weather(@Json(name = "Temp") val temperature: Array<String>)
class Airport(
    @Json(name = "IATA") val code: String,
    @Json(name = "Name") val name: String,
    @Json(name = "Delay") val delay: Boolean,
    @Json(name = "Weather") val weather: Weather
) {
    companion object {
        //airport데이터 가지고옴
        fun getAirportData(code: String): Airport? {
            val url = "https://soa.smext.faa.gov/asws/api/airport/status/$code"
            return Klaxon().parse<Airport>(URL(url).readText()) //text 응답 추출 후 JSON 컨텐츠를 Airport의 인스턴스롤 Parse
        }
    }
}

Airport 클래스에서 @Json 어노테이션을 이용해 JSON 응답이 클래스내의 프로퍼티에 맵핑 되도록 만든다. getAirportData() 메소드에서 우리는 데이터를 가지고 오고, text 응답을 추출한 다음 JSON 컨텐츠를 Airport의 인스턴스로 parse한다.

주어진 공항코드 리스트를 가지고 방금 설명한 메소드를 이용해서 데이터를 순차적으로 내려받는다.

Airport

fun main() {
    val format = "%-10s%-20s%-10s"
    println(String.format(format, "Code", "Temperature", "Delay"))
    val time = measureTimeMillis {
        val airportCodes = listOf("LAX", "SFO", "PDX", "SEA")
        val airportData: List<Airport> =
            airportCodes.mapNotNull { anAirportCode ->
                Airport.getAirportData(anAirportCode)
            }
        airportData.forEach { anAirport ->
            println(String.format(format, anAirport.code, anAirport.weather.temperature.get(0), anAirport.delay))
        }

    }
    println("Time take $time ms")
}

measureTimeMillis() 함수를 이용해서 코드의 작업에 걸린는 시간도 측정한다.

💻 출력

Code      Temperature         Delay     
LAX       53.0 F (11.7 C)     false     
SFO       52.0 F (11.1 C)     false     
PDX       38.0 F (3.3 C)      false     
SEA       36.0 F (2.2 C)      false     
Time take 3982 ms

모든 공항은 연착이 없고 프로그램은 실행에 4초 가까이 걸렸다. 실행속도는 실행시키는 시점의 네트워크 성능에 따라서 달라진다.

비동기로 만들기

이전 코드에서 getAirportData() 메소드 호출이 차단방식이었다. "LAX"에 대한 정보를 얻으려고 호출했을 때, 프로그램은 해당 요청이 완료될 때까지 다음 코드인 "SFO"에 대한 요청을 하지 않는다. 이런 호출들은 비차단방식으로 만들 수 있다. 즉, "LAX" 에 대한 요청이 완료될 때까지 다른 요청을 안하고 기다릴 필요가 없다는 뜻이다.

🔎 비차단 방식

  1. main()함수가 비동기 실행을 완료하기 위해서 차단하고, 대기하기 위해서 runBlocking()을 main() 전체에 적용시킨다.
  2. Airport의 getAirportData()는 비차단방식으로 호출하기 위해서 runBlocking()을 사용할 수 없고 launch()는 로직을 수행하기 위한 함수이지 결과를 리턴하는 함수가 아니기 때문에 async()가 적절한 선택이다.

만일 async()가 반복안에서 직접 사용되면 코루틴은 현재 코루틴 컨텍스트에서 동작을 하게 된다. 그렇게되면 main 스레드에서 실행하게 될 것이다.
getAirportData() 메소드 호출은 비차단방식이 될 것이고, 동시 실행이 된다. 즉, main 스레드는 차단 되지도 않고, 대기하지도 않는다. 하지만 호출의 실행은 main 스레드에서 인터리브되며 실행된다. 이런 동작은 비차단방식으로 동작하는 장점을 주었지만 성능적으로는 이점이 전혀 없다.

async()를 호출할 때 다른 컨텍스트를 사용하면 해결 가능하다. async()에 Dispatcher.IO 같은 CoroutineContext를 전달하면 된다.

Airport

fun main() = runBlocking {
    val format = "%-10s%-20s%-10s"
    println(String.format(format, "Code", "Temperature", "Delay"))
    val time = measureTimeMillis {
        val airportCodes = listOf("LAX", "SFO", "PDX", "SEA")
        val airportData: List<Deferred<Airport?>> =
            airportCodes.map { anAirportCode ->
                async(Dispatchers.IO) {
                    Airport.getAirportData(anAirportCode)
                }
            }
        airportData.mapNotNull { anAirportData -> anAirportData.await() }
            .forEach { anAirport ->
                println(String.format(format, anAirport.code, anAirport.weather.temperature.get(0), anAirport.delay))

            }

    }
    println("Time take $time ms")
}

두 버전의 코드의 구조는 똑같지만, 약간의 차이점이 있다.

  1. List<Airport>대신 List<Deffered>Airport>>를 만들었다.
    async()의 결과는 Deffered<T>의 인스턴스이기 때문이다.

  2. 첫 번째 반복에서 async() 메소드에 전달된 람다 내부의 getAirportData()를 호출했다.
    async()는 호출된 직후 바로 리스트에 저장된 Deffered<Airport?>를 리턴한다.

  3. 두 번째 반복에서 Deffered<T> 리스트를 반복하면서 await()를 실행하여 데이터를 가져온 await()를 호출하면 실행의 흐름을 차단하지만 실행의 스레드는 차단하지 않는다. main 스레드는 await() 호출에 도달한 이후에 코루틴을 실행시킨다. Dispatchers.IO 풀이 스레드가 웹 서비스에 요청하는 동안 main 스레드가 쉬고있다.

💻 비동기 버전 출력

Code      Temperature         Delay     
LAX       53.0 F (11.7 C)     false     
SFO       52.0 F (11.1 C)     false     
PDX       39.0 F (3.9 C)      false     
SEA       36.0 F (2.2 C)      false     
Time take 2136 ms

공항 데이터는동일하게 가져오지만 실행 속도는 비동기 버전이 1초 정도 빨리 완료되었다. 그러나 네트워크 속도를 신뢰할 수 없기 때문에 실행 속도의 차이를 확인할 때는 주의해야 한다.

📌 예외 처리


웹 서비스에 요청을 할 때, DB를 업데이트할 때, 파일에 액세스할 때 등 여러 상황에서 많은것들이 계획대로 흘러가지 않는다. 예외를 다루는 방법은 코루틴을 어떻게 시작하냐에 달려있다. launch()와 async() 중 무엇을 사용해야할까?

launch와 Exception

launch를 사용했다면 호출자는 예외를 받을 수 없다. launch()는 실행 후에 코루틴이 완료될 때까지 기다리는 방법이 있긴 하지만 보통 fire & forget 모델을 사용한다.

공항 상태에 잘못된 공항 코드를 넣어보겠다.

LaunchErr

fun main() = runBlocking {
    try {
        val airportCodes = listOf("LAX", "SF-", "PD-", "SEA")
        val jobs: List<Job> = airportCodes.map { anAirportCode ->
            launch(Dispatchers.IO + SupervisorJob()) {
                val airport = Airport.getAirportData(anAirportCode)
                println("${airport?.code} delay ${airport?.delay}")
            }
        }
        jobs.forEach { it.join() }
        jobs.forEach { println("Canceled: ${it.isCancelled}") }
    } catch (ex: Exception) {
        println("ERROR: ${ex.message}")
    }
}

두 개의 적절한 공항코드를 사용하고, 두 개는 부적절한 공항코드를 사용했다. 이번 예제에서는 예외를 어떻게 다루는지 복 위해서 async()를 사용하지 않고 launch()를 사용했다.

launch() 함수는 코루틴이 시작됐다는 의미를 가지는 Job 객체를 리턴 한다. Job 객체는 코루틴이 종료되는 것을 기다리는데 사용한다. 그리고 Job 객체의 isCancelled 프로퍼티를 이용해서 작업이 성공적으로 완료되었는지 아니면 실패가 일어나서 취소되었는지 확인한다.

💻 ERROR

Exception in thread "DefaultDispatcher-worker-4" Exception in thread "DefaultDispatcher-worker-3" com.beust.klaxon.KlaxonException: Unable to instantiate Airport:No argument provided for a required parameter....

코루틴이 launch()를 이용해서 실행되렀기 때문에 예외를 호출자에게ㅈ까지 전파하지 못하기 때문이다. 이는 launch()의 특성이다.

launch()를 사용한다면, 예외 핸들러를 반드시 설정해야 한다. CoroutineExceptionHandler 예외 핸들러가 등록되어 있다면 컨텍스트 세부사항과 예외 정보와 함께 핸들러가 트리거된다.

LaunchErrHandler

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { context, ex ->
       println("Caught: ${context[CoroutineName]} ${ex.message?.substring(0..28)}")
    }
    try {
        val airportCodes = listOf("LAX", "SF-", "PD-", "SEA")
        val jobs: List<Job> = airportCodes.map { anAirportCode ->
            launch(Dispatchers.IO + CoroutineName(anAirportCode) + handler + SupervisorJob()) {
                val airport = Airport.getAirportData(anAirportCode)
                println("${airport?.code} delay ${airport?.delay}")
            }
        }
        jobs.forEach { it.join() }
        jobs.forEach { println("Canceled: ${it.isCancelled}") }
    } catch (ex: Exception) {
        println("ERROR: ${ex.message}")
    }
}
  1. 핸들러를 만들었다.
    핸들러는 실패한 코루틴의 컨텍스트를 프린터한다.
  2. launch()를 호출할 때 핸들러를 등록했다.
    쉽게 알아보기 위해서 코루틴의 이름도 등록했다.

💻 출력

Caught: CoroutineName(SF-) Unable to instantiate Airport
Caught: CoroutineName(PD-) Unable to instantiate Airport
LAX delay false
SEA delay false
Canceled: false
Canceled: true
Canceled: true
Canceled: false

예외가 등록된 핸들러에 의해서 우아하게 처리되었다. 코드의 실행 결과는 우리가 아직 다루디 않은 코루틴의 또 다른 동작을 보여준다. 코루틴이 처리되지 않은 에외로 실패하게 되면 코루틴은 취소된다.

async와 Exception

launch()는 예외를 호출자에게 전파하지 않는다. 그래서 launch()를 사용할 땐 반드시 예외 핸들러를 등록해야 한다. 하지만 async() 함수는 Deffered<T> 인스턴스를 리턴한다. Deffered<T> 인스턴스는 await()가 호출되면 호출자에게 예외를 전달해준다.

launch()에 부적절한 공항 코드를 사용하는 대신 async()를 사용해서 두 함수가 어떻게 예외가 다르게 처리되는지 확인해보자. await()를 호출할 때 try-catch를 사용해서 예외를 처리한다.

AsyncErr

fun main() = runBlocking {
        val airportCodes = listOf("LAX", "SF-", "PD-", "SEA")
        val airportData = airportCodes.map { anAirportCode ->
            async(Dispatchers.IO + SupervisorJob()) {
                Airport.getAirportData(anAirportCode)
            }
        }
        for (anAirportData in airportData) {
            try {
                val airport = anAirportData.await()
                println("${airport?.code} ${airport?.delay}")
            } catch (ex: Exception) {
                println("ERROR: ${ex.message?.substring(0..28)}")

            }
        }
    }

첫 번째 반복에서, map()을 사용하고, 함수형 스타일로 작성했다. 하지만 두 번째 반복에서는 Deffered<T> 리스트의 요소들을 반복했다. 이는 함수형 스타일인 forEach()가 아니고 명령형 스타일의 for 반복문이다. 여기서는 forEach()와 람다를 사용하면 간결하지 않고 기독성이 좋지 않기 때문에 for문에 좋은 선택이다.

💻 출력

LAX false
ERROR: Unable to instantiate Airport
ERROR: Unable to instantiate Airport
SEA false

async() / await()는 비동기 호출을 쉽게 만들어준 뿐만 아니라 예외처리도 잘한다.

📌 취소와 타입아웃


코루틴을 취소하면 코루틴 내의 코드는 더 이상 실행되지 않는다. 코루틴의 취소는 Java에서 사용하던 '스레드 종료'와는 연관성이 없다. 취소는 가볍고 컨텍스트를 동유하는 코루틴의 계층구조에 영향을 끼친다.

launch()가 리턴하는 Job 객체와 async()가 리턴하는 Deffered<T> 객체에는 각각 cancel() 메소드와 cancelAndJoin() 메소드가 있다. 이 메소드들은 명시적으로 콭루틴을 취소할 수 있다. 하지만 코루틴은 현재 서스펜션 포인트에 있는 경우에만 취소가 가능하다. 코루틴이 바쁘게 동작 중이라면 취소 알림을 받지 못하고 빠져나오지 못하게 된다.

코틀린은 컨텍스트를 공유하는 다수의 코루틴이 계층관계를 구성할때 구조적 동시성을 제공해준다.

  • 코루틴에서 컨텍스트를 공유하는 새로운 코루틴을 생성하면 새 코루틴은 기존 코루틴의 자식으로 간주된다.
  • 부모 코루틴은 자식 코루틴이 완료되어야만 완료될 수 있다.
  • 부모 코루틴을 취소하면 자식 코루틴이 취소된다.
  • 서스펜션 포인트에 진입한 코루틴은 서스펜션 포인트에서 던져진 CancellationException을 받을 수 있다.
  • 실행되고 있는 코루틴이 서스펜션 포인트에 진입핮 않은 경우 isActive 프로퍼티를 체크해 동작 중에 취소되었는지 여부를 확인하 수 있다.
  • 정리해야 할 자원을 가진 코루틴은 finally 블록에서 자원을 정리해야 한다.
  • 처리되지 않은 예외는 코루틴을 취소시킨다.
  • 자식 코루틴이 정지하면 부모 코루틴이 정지하므로 형제 코루틴도 취소되고 만다. 이런 동작은 부모에서 자식으로만 단방향으로 취소가 가능하게 만드는 슈퍼바이저 잡을 통해서 변경 할 수 있다.

코루틴 취소

코루틴의 작업이 완료되던 말던 상관이 없다면 Job 또는 Deferred<T> 인스턴스의 cancel() 메소드나 cancelAndJoin() 메소드를 이용해서 코루틴을 취소시킬 수 있다.

바쁘게 동작 중 이라면, 위 메소드는 해당 동작을 방해하지 못한다. 반면에 코루틴이 yield(), delay(), await()같은 서스펜션 포인트에 진입했다면 CancellationException을 발생시킨다.

긴 연산을 수행하면 한다면 빈번하게 중단점을 두면서 코루틴의 isActive 프로퍼티가 true인지를 확인하도록 구조를 만들어야 한다. isActive가 false라면 연산을 중단하고 취소요청을 받아들이게 된다.

코루틴 내부에서 호출한 함수가 차단된 상태이거나 서스펜션 포인트가 없을때는 isActive 프로퍼티를 체크하는 기능을 만들 필요가 없다. 아런 상황에는 차단된 호출을 다른 델리게이트해서 우회시키고 기다려야한다.

compute() 함수를 만들어보자
파라미터가 true로 전달 될 경우 긴 연산 중에서 IsActive 프로퍼티를 확인하게 된다. 파라미터가 false일 경우 긴 시간 동안 그냥 실행된다. isActive 프로퍼티에 접근을 해야하기 때문에 코드는 코루틴의 컨텍스트에서 동작해야한다. 이를 위해서 compute() 함수 내부의 코드를 호출자의 스코프를 운반하는 coroutineScope()의 호출로 감싸도록 한다.

compute()

suspend fun compute(checkActive: Boolean) = coroutineScope {
    var count = 0L
    val max = 10000000000
    while (if (checkActive) {
            isActive
        } else (count < max)
    ) {
        count++
    }
    if (count == max) {
        println("compute, checkActive $checkActive ignored cancellation")
    } else {
        println("compute, checkActive $checkActive bailed out early")
    }
}

연산이 길게 실행된다면 isActive를 체크해봐야 한다. 만약 코루틴 안에서 호출한 함수의 연산이 길고 도중에 방해할 수 없다면 코루틴 또한 방해할 수 없다.

fetchResponse()


val url = "https://httpstat.us/200?sleep=1000"
fun getResponse() = java.net.URL(url).readText()
suspend fun fetchResponse(callAsync: Boolean) = coroutineScope {
    try {
        val response = if (callAsync) {
            async { getResponse() }.await()
        } else {
            getResponse()
        }
        println(response)
    } catch (ex: CancellationException) {
        println("fetchResponse called with call Async $callAsync: ${ex.message}")
    }
}

fetchResponse() 함수는 sleep 파라미터의 크기만큼의 밀리초가 지나면 특정 HTTP코드를 리턴하는 URL 요청을 보낸다. callAsync 파라미터가 false라면 URL호출을 동기화해서 실행한다. 그렇기 때문에 호출자를 차단하고 취소를 허용하지 않게 된다. 파라미터가 true라면 URL 호출은 비동기로 진행된다. 그렇기 때문에 코루틴이 대기상태라면 바로 취소를 할 수 있다.

runBlocking {
    val job = launch(Dispatchers.Default) {
        launch { compute(checkActive = false) }
        launch { compute(checkActive = true) }
        launch { fetchResponse(callAsync = false) }
        launch { fetchResponse(callAsync = true) }
    }
    println("Let them run ..")
    Thread.sleep(1000)
    println("OK, that's enough, cancel")
    job.cancelAndJoin()
}

checkActive가 true로 호출된 compute() 메소드는 isActive 프로퍼티를 체크하기 때문에 취소 명령이 내려지면 곧바로 종료될 것이다. checkActive가 false로 호출된 compute() 메소드는 부모 코루틴이 내린 취소 명령을 무시한다.

💻 출력

LAX false
ERROR: Unable to instantiate Airport
ERROR: Unable to instantiate Airport
SEA false

방해금지

종종 작업을 방해받기를 원하지 않는 경우가 았다. 중요한 코드에 yield(), delay(), await() 같은 서스펜션 포인트가 존재한다면, 작업 중간에 취소되거나 방해받길 원치를 않을 것이다. withContext(NonCancellable) 는 방해금지를 설정해준다.

방해금지

suspend fun doWork(id: Int, sleep: Long) = coroutineScope {
    try {
        println("$id: entered $sleep")
        delay(sleep)
        println("$id: finished nap $sleep")
        withContext(NonCancellable) {
            println("$id: do not disturb, please")
            delay(5000)
            println("$id: OK, you can talk to me how")
        }
        println("$id: outside the restricted context")
        println("$id: isActiveL $isActive")
    } catch (ex: CancellationException) {
        println("$id: doWork($sleep) was cancelled")
    }
}

doWork() 함수는 호출자의 코루틴의 컨텍스트에서 실행된다. 함수는 입력된 시간만큼 코루틴을 지연시키며 이 지연은 취소가 가능하다. 그 후에 NonCancellable 컨텍스트로 들어가며 이 때 발생하는 지연은 취소가 불가능하다. 이 시점에서 발생하는 인터럽트는 모두 무시된다. 하지만 취소 명령이 들어오면 isActive 프로퍼티는 변경된다.

runBlocking {
    val job= launch(Dispatchers.Default){
        launch { doWork(1,3000) }
        launch { doWork(2,1000) }
    }
    Thread.sleep(2000)
    job.cancel()
    println("cancelling")
    job.join()
    println("done")
}

💻 출력

1: entered 3000
2: entered 1000
2: finished nap 1000
2: do not disturb, please
cancelling
1: doWork(3000) was cancelled
2: OK, you can talk to me how
2: outside the restricted context
2: isActiveL false
done

양방향 취소

코루틴이 코드에서 처리해놓은 cancellation 예외가 아닌 다른 예외를 만난다면, 코루틴은 자동으로 취소된다. 코루틴의 부모 코루틴도 취소된다. 부모 코루틴이 취소될 때, 모든 자식 코루틴도 취소된다.

예외

suspend fun fetchResponse(code: Int, delay: Int) = coroutineScope {
    try {
        val response = async {
            URL("https://httpstat.us/$code?sleep=$delay").readText()
        }.await()
        println(response)
    } catch (ex: CancellationException) {
        println("${ex.message} for fetchResponse $code")
    }
}

runBlocking {
    val handler = CoroutineExceptionHandler { _, ex ->
        println("Exception handled: ${ex.message}")
    }
    val job = launch(Dispatchers.IO + SupervisorJob() + handler) {
        launch { fetchResponse(200, 5000) }
        launch { fetchResponse(202, 1000) }
        launch { fetchResponse(404, 2000) }

    }
    job.join()
}

주어진 코드가 404일 경우, 서비스 요청이 예외와 함께 실패한다. 함수가 예외를 처리하고 있지 않으면서 함수를 실행중인 코루틴은 취소된다.

💻 출력

202 Accepted
Parent job is Cancelling for fetchResponse 200
Exception handled: https://httpstat.us/404?sleep=2000

202 코드를 요청한 코루틴은 1초만에 완료되었다. 404를 요청한 코루틴은 실패한것을 볼 수 있다.

슈퍼바이저 잡

코루틴에 handler를 전달한 방법처럼, 인스턴스를 전달할 수도 있다. 예를 들면 launch(coroutineContext + supervisor) 형태로 SupervisorJob에서 supervisor 인스턴스를 사용할 수도 있다.
두 경우 보두 슈퍼바이저가 적용된 자식이 취소된다고 부모는 취소되지 않는다. 하지만 부모가 취소되면 자식도 취소된다.

supervisorScope

suspend fun fetchResponse(code: Int, delay: Int) = coroutineScope {
    try {
        val response = async {
            URL("https://httpstat.us/$code?sleep=$delay").readText()
        }.await()
        println(response)
    } catch (ex: CancellationException) {
        println("${ex.message} for fetchResponse $code")
    }
}

runBlocking {
    val handler = CoroutineExceptionHandler { _, ex ->
        println("Exception handled: ${ex.message}")
    }
    val job = launch(Dispatchers.IO + handler) {
        supervisorScope {
            launch { fetchResponse(200, 5000) }
            launch { fetchResponse(202, 1000) }
            launch { fetchResponse(404, 2000) }
        }
    }
    Thread.sleep(4000)
    println("200 should still be running at this time")
    println("let the parent cancel now")
    job.cancel()
    job.join()
}

3개의 중첩된 launch() 호출을 supervisorScope() 호출로 감쌌다. 코루틴은 4초동안 실행되고 202 코드를 처리하는 코루틴을 그 시간동안 완료되모 404 코드는 취소될 것이다. 200 코드를 처리하는 코루틴이 시행되는 5초동안 영향을 받으면 안된다. 코루틴은 완료될때까지 형제 코루틴에 영향을 받지 않는다. 하지만 4초가 지난 후 부모 코루틴을 취소시키면 자식 코루틴도 취소되게 한다.

💻 출력

202 Accepted
Exception handled: https://httpstat.us/404?sleep=2000
200 should still be running at this time
let the parent cancel now
StandaloneCoroutine was cancelled for fetchResponse 200

명확하게 독립된 작업을 하는 자식 코루틴의 탑다운 계층구조를 구성하고 싶을 때만 코루틴에 슈퍼바이저를 적용해야 한다.

타임아웃을 이용한 프로그래밍

코루틴은 긴 시간 연계하면서 동작하는 작업에 적합하다. 하지만 우리는 적절하지 않은 긴 시간 혹은 영원히 기다리기를 원하지는 않는다.
코루틴이 완료될 때까지 주어진 시간 이상의 시간이 사용된다면 CancellationException의 하위클래스인 TimeoutCancellationException을 받게된다. 그 결과 주어진 시간 이상이 걸리면 작업은 취소된다.

supervisorScope

suspend fun fetchResponse(code: Int, delay: Int) = coroutineScope {
    try {
        val response = async {
            URL("https://httpstat.us/$code?sleep=$delay").readText()
        }.await()
        println(response)
    } catch (ex: CancellationException) {
        println("${ex.message} for fetchResponse $code")
    }
}

runBlocking {
    val handler = CoroutineExceptionHandler { _, ex ->
        println("Exception handled: ${ex.message}")
    }
    val job = launch(Dispatchers.IO + handler) {
        withTimeout(3000) {
            launch { fetchResponse(200, 5000) }
            launch { fetchResponse(202, 1000) }
            launch { fetchResponse(404, 2000) }
        }
    }
    job.join()
}

💻 출력

202 Accepted
Parent job is Cancelling for fetchResponse 200
java.io.FileNotFoundException: https://httpstat.us/404?sleep=2000

3초보다 적게 걸린 코루틴은 성공적으로 완료되었고 3초이상 걸린 코루틴은 취소되었다.







🔑 정리


코루틴은 프로그램을 비동기적으로 실행할 때뿐만 아니라 현명하고 쉽게 예외를 처리하는데도 좋은 방법을 제공해준다.

코루틴은 동시실행 코드, 비동기 코드를 동기 코드, 순차적 코드와 유사한 구조로 유지하게 도와준다.

타임아웃을 사용하면 실행시간을 제어할 수 있도 슈퍼바이저 잡을 설정하면 계층구조 내의 코루틴의 상호작용을 제어할 수 있다.



출처 : 다재다능 코틀린 프로그래밍

profile
개발하고싶은사람

0개의 댓글