funmain() = runBlocking<Unit> { val time = measureTimeMillis { val one = doSomethingUsefulOne() val two = doSomethingUsefulTwo() println("The answer is ${one + two}") } println("Completed in $time ms") }
suspendfundoSomethingUsefulOne(): Int { // pretend we are doing something useful here delay(1000L) return13 }
suspendfundoSomethingUsefulTwo(): Int { // pretend we are doing something useful here, too delay(1000L) return29 }
코루틴에서는 일반 코드처럼 작성하면 비동기적인 기능일지라도 순차적으로 실행한다. 즉, 위의 코드는
doSomethingUsefulOne() 실행
doSomethingUsefulTwo() 실행
println("The answer is ${one + two}") 실행
순서로 진행된다.
Concurrent using async
만약 두 연산에 dependency가 없다면 동시에 실행하는 것이 리소스를 효율적으로 사용할 수 있을 것이다. 아래 코드를 참고하자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
funmain() = 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") }
suspendfundoSomethingUsefulOne(): Int { delay(1000L) // pretend we are doing something useful here return13 }
suspendfundoSomethingUsefulTwo(): Int { delay(1000L) // pretend we are doing something useful here, too return29 }
코루틴 빌더인 async 키워드를 사용하면 비동기를 비동기처럼(?) 동작하게 할 수 있다.
funmain() = 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") }
suspendfundoSomethingUsefulOne(): Int { delay(1000L) // pretend we are doing something useful here return13 }
suspendfundoSomethingUsefulTwo(): Int { delay(1000L) // pretend we are doing something useful here, too return29 }
async 의 매개변수에 start = CoroutineStart.LAZY 를 추가하게 되면 해당 코루틴은 바로 실행되지 않는다. LAZY 가 걸린 코루틴을 start() 하거나 await() 를 해 줄 때 비로소 값을 받을 수 있다.
async 는 인자를 받을 수 있는데, 아무 인자도 넣지 않으면 바로 실행하는(start = CoroutineStart.Default) 것이 default이다. 즉, start() 나 await() 를 만나지 않아도 내부적으로는 이미 그 코루틴이 실행된 상태라는 의미이다. 다음의 코드를 실행해 보면 이해가 조금 쉬울 듯하다.
funmain() = runBlocking<Unit> { var a = 0 var b = 0 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") }
suspendfundoSomethingUsefulOne(): Int { delay(1000L) // pretend we are doing something useful here return13 }
suspendfundoSomethingUsefulTwo(): Int { delay(1000L) // pretend we are doing something useful here, too return29 }
참고
start()
코루틴을 시작한다. 시작할 수 있으면 true, 없으면 false 를 반환한다.
await()
이미 start() 된 코루틴의 경우 실행된 값을 반환하고, start() 되기 전 코루틴이라면 실행 및 값을 반환한다.
Async-style functions
이렇게 사용하지 마라고 권고하는 내용의 예제이다. async-style의 함수는 exception이 발생했을 때 돌이킬 수 없는 상황에 봉착하게 된다. (exception이 발생되어도 코루틴이 죽지 않고 좀비로 남는 모습을 볼 수 있음.)
// note that we don't have `runBlocking` to the right of `main` in this example funmain() { val time = measureTimeMillis { // somethingUseful~ 함수는 suspend 함수가 아님(누구나 실행 가능) 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") }
funmain() = runBlocking<Unit> { val time = measureTimeMillis { println("The answer is ${concurrentSum()}") } println("Completed in $time ms") }
// coroutineScope로 감싸서 suspend function으로 바꿔주었다. // suspend function은 아무 곳에서나 호출할 수 있는 형태가 아닌 코루틴 내에서만 사용 가능 suspendfunconcurrentSum(): Int = coroutineScope { val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } one.await() + two.await() }
suspendfundoSomethingUsefulOne(): Int { delay(1000L) // pretend we are doing something useful here return13 }
suspendfundoSomethingUsefulTwo(): Int { delay(1000L) // pretend we are doing something useful here, too return29 }
위의 예제처럼 scope 안에서 suspending function들을 조립해서 사용해야 한다. exception이 발생할 경우 코루틴 블록 내부의 모든 코루틴이 취소되어 우리의 코드는 안전할 것임. 코루틴 좀비 해결!
Cancellation propagated coroutines hierarchy
아래 예제는 async 로 작성된 코루틴에서 exception이 발생할 경우 어떤 결과가 나타날지 시뮬레이션해 보는 코드이다.
suspendfunfailedConcurrentSum(): 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() }
asnyc 로 실행된 코루틴 중 하나의 코루틴에서 exception이 발생할 경우, 이 exception은 다른 코루틴에게 영향을 주어 결국 모든 코루틴이 중단되게 한다.
정리
일반 코드처럼 코틀린 코드를 작성할 경우 순서대로 동작한다.
일반 비동기 코드처럼 동시에 실행하고 싶다면 코루틴 빌더인 async 를 이용하자.
코루틴을 일반 함수로 감싸 아무 곳에서나 실행할 수 있는 미친 짓은 절대로 하지 말자. (exception 터질 때 감당 불가)