Kotlin Coroutines #1



reference: 

https://kotlinlang.org/docs/reference/coroutines.html

https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html





 Coroutines

Android의 main thread에서는 UI 조작만을 권장하기 때문에(UI가 멈추는걸 좋아하는 사용자는 없겠죠), disk IO나 http request 같은 작업은 다른 thread로 부터 수행되어야 합니다. 일반적인 경우라면 thread with handler나 AsyncTask를 사용하게 될 텐데요, 이해는 됩니다만 적어야 하는 소스의 양이 많아 지죠. 이런 비동기 작업들을 좀 더 효율적으로 작성하기 위해 언어 차원에서 제공되는 기능으로 보입니다. 


 Suspend

여러 작업들을 하다보면 잠시 중단하여 다른 작업을 해야할 때가 있습니다. Coroutine은 thread 위에서 동작하며, thread를 block하지 않고 suspend합니다. Thread blocking 및 context switch는 비용이 비싸기 때문에 간단한 작업을 하려는 것에 비해 지나친 시간이 소요되곤 합니다. 반면 coroutine의 suspend는 거의 cost-free하다고 합니다.

(https://stackoverflow.com/questions/43021816/difference-between-thread-and-coroutine-in-kotlin)


 launch {}

간단한 예제로 실습해보았습니다.

@Test fun corountine_test() {
println("start")

// do things on default coroutine(shared pool of threads)
launch {
// delay doesn't stop the thread, but only suspends the coroutine itself (non-blocking)
// when coroutine is being suspended, the thread is returned to the pool till it's needed again
// and when coroutine is done waiting, it finds a free thread in the pool and resumes
delay(1000)
println("hello")
}

// when we want to do delay on main thread
runBlocking {
delay(2000)
}
println("stop")
}

Coroutine은 launch로 시작되며, 명시하지 않으면 기본적으로 shared thread pool의 임의의 thread에서 수행됩니다. 

delay()는 앞서 언급한 suspend의 기능을 수행합니다. Thread.sleep()의 전통적인 방식에 비해 매우 적은 자원을 소모하고, thread를 block하지 않습니다.

Coroutine이 suspend되면 수행되던 thread는 다시 pool로 반환되며, 필요해질 때 다시 불러진다고 합니다. Thread pool 관리 차원에서도 효율적이라고 볼수 있겠습니다. Suspend가 종료되면, 다시 pool에서 가용한 thread를 찾아 작업을 재개합니다.

또한 delay()는 coroutine에서만 사용할 수 있기 때문에, 일반적인 thread 상의 코드에서는 사용이 불가합니다. 따라서 위와 같이 runBlocking {}을 통해 호출하여야 합니다.


Cost

아래 예시로 coroutine의 효율을 간단하게 알아보았습니다:

@Test fun is_coroutine_cheaper() {
val c = AtomicInteger()
for (i in 1..1_000_000) {
launch {
c.addAndGet(i)
}
}
// not all of coroutines are completed, c is not what we expect
println(c.get())
}

위 코드의 수행시간은 1초 미만이었습니다. 100만개의 coroutine을 생성하여 작업하였으나 매우 적은 시간이 소요되었음을 알 수 있었는데요, thread로 수행한다면 분명 수 분의 시간이 소요됐을 것 같습니다.


 async {}

async {}는 기본적으로 launch {}와 같은 동작을 하지만 Deffered<T>를 반환합니다. (Deffered<T>는 아주 간단한 future라고 합니다)

@Test fun async_test() {
val deferred: List<Deferred<Int>> = (1..1_000_000).map { n ->
async {
delay(1000) // to make sure coroutines run parallel
n
}
}

runBlocking {
// await has to be called in coroutine
val sum = deferred.sumBy { it.await() }
println("sum : $sum")
}
}

위 예제는 앞서와 같이 100만개의 async {} 를 통한 coroutine을 수행하는 코드입니다. 다만 여기서의 차이점은 저 작업들이 완료되기까지 기다린다는 점인데요, await()를 통해 작업이 완료되기 까지 기다리게 하였습니다. 

100만개의 coroutine들이 과연 parallel하게 수행되는지 확인하기 위하여 각 작업마다 1초를 delay했습니다. 수행시간은 10초가 소요되지 않았습니다.

역시, coroutine의 기능은 coroutine 안에서 호출되어야 하므로 await()는 runBlocking {}안에서 수행했습니다. 


Suspend function

만약 위 예제에서 async {}안의 작업들을 하나의 함수로 표현하고 싶다면?

async {
workload(n)
}

fun workload(n: Int): Int {
delay(1000)
return n
}

위와 같은 코드가 될 것 같습니다. 하지만 앞서 말한 바와 같이, delay()는 coroutine의 기능이기 때문에 일반적인 함수에서는 호출이 불가합니다. 컴파일이 되지 않네요.

이럴 때 쓰는 것이 suspend 키워드입니다. fun 앞에 suspend를 붙이기만 하면 됩니다.


suspend fun workload(n: Int): Int {
delay(1000)
return n
}


'programming > android' 카테고리의 다른 글

기본적인 메모리 관리  (0) 2018.03.06
Splash 화면 구성하기  (3) 2018.02.20
Kotlin Coroutines #2  (0) 2018.01.28
Tools로 xml layoyt의 preview 제대로 표시하기  (0) 2018.01.22
Kotlin Standard Functions  (0) 2018.01.22

+ Recent posts