요즘 코루틴이란 이야기를 많이 접하게 되어 한번 공부를 해보았습니다.
코루틴이란 개념은 오래전부터 사용된 것으로 보입니다. (공부하면서 구글링을 해봤는데 2016년도 글도 있고 2014년도 글도 있었습니다.)
그리고 코루틴을 공부하는 이유는 비동기처리에 rx가 현재 많이 쓰이고 있지만 Android 진영에서 많은 부분에서 rx를 빼고 코루틴으로 변경되어 지고 있기 때문에 그 활용을 알고 써봐야한다고 생각하였기 때문입니다.
코루틴?
코루틴을 알기 전에 서브루틴이라는 것부터 확인해야 할것 같습니다. 서브루틴이란 어떤 동작을 하기위해 함수가 호출이 되었고 그 함수의 동작이 끝나면 자신을 호출하였던 메인루틴으로 돌아오는 경우를 말합니다. 만약 함수가 어떤 값을 반환(Return) 한다면 메인루틴에서 그 값을 받아 활용합니다. (즉 메인 루틴은 메인 스레드라고 생각하는게 좋을것 같네요 ㅎ, 그리고 우리가 일반적으로 코딩할때 쓰는 루틴입니다.)
그럼 코루틴이란 간단하게 하나의 진입점과 하나의 탈출점이 있는 서브루틴과 다르게 다양한 진입점과 다양한 탈출점이 있는 루틴이라 생각하면 됩니다. 즉, 메인 루틴에서 어떤 함수를 호출하고 그 함수가 동작 하다 중간에 멈추고 다시 메인 루틴의 동작을 하다 다시 멈춘 함수로 돌아와서 마저 작업을 하게 됩니다. 사실 이렇게 표현하면 이해가 되지 않을 것인데.... 이게 가장 적절한 표현 인것 같습니다.
간단한 비유로 표현하자면 양쪽의 도화지에 한사람이 한손으로 펜을 잡고 쪼금씩 그림을 그리는것을 생각하면 될듯합니다. (멀티 쓰레드로 생각하면 양쪽 도화지에 한사람이 양손으로 펜을 잡고 동시에 그림을 그린다고 생각하고 비교하시면 될 것 같네요. 이 비유는 아래 쾌락코딩님의 글에서 가져왔습니다. 가장 쉽게 이해할 수 있는 비유라 생각이 되네요)
아 그리고 쾌락코딩님의 글에서 발취한 내용인데 메모리적으로 코루틴은 매우 가볍습니다!
Kotlin 문서에서 코루틴 익혀보기
우선 안드로이드에서 사용해보기 전에 기본적으로 어떻게 사용하는지 코틀린 문서의 코루틴 기초 부분을 확인 해보았습니다. (이 부분은 Android Studio가 아닌 InteliJ를 사용했습니다.)
우선 kotlin에서 말하는 Asynchronus Programing Technique들에는 여기서 확인해볼 수 있구요 그 안에 coroutines가 존재 합니다.
코틀린에서 말하는 Coroutines은 idea of suspendable computations, i.e. the idea that a function can suspend its execution at some point and resume later on 이렇게 말하고 있습니다. (번역하면 일시 중단 가능한 컴퓨터 계산 방식의 아이디어로 어떤 시점에서 동작하다 멈출수있고 후에 다시 동작할 수있는 개념이라고 번역이 되겠네요. 구글 번역아니고 ... 제 번역이라.. 틀릴 지도..)
자 그럼 익혀봅시다!
우선 프로적트를 만듭니다. (물론 InteliJ에서 말이죠 ㅎㅎ)
이렇게 kotlin 프로젝트를 생성하시고 build.gradle에
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
}
와
repositories {
jcenter()
}
추가 합니다. (만일 Maven을 사용하시면... 공식문서의 여기에서 한번 다른 설치방법을 확인 해보시면 될것 같습니다.)
코루틴은 Thread처럼 parallel하게 동작할 수있고, 기다리고, 통신할 수 있습니다. 하지만 Thread와 가장큰 차이는 그 사용비용이 매우 저렴하다는 것입니다.(almost free라 적혀있는거 보니 오우야 진짜 사용 부담이 적은것 같습니다. 아 그리고 이미 위에서 말했군요 ㅎㅎ)
자 그럼 코루틴은 어떻게 시자하느냐? 코루틴은 launch{ } 함수에서 시작합니다. 기본적으로 코루틴은 shared pool of threads 에서 구동됩니다. 기본적으로 코루틴으로 작성된 프로그램에서 여전히 Thread는 존재하지만 하나의 Thread로 많은 코루틴을 실행할 수 있기 때문에 많은 Thread가 필요없게 됩니다.
다음 코드를 보면
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
fun main(){
println("Start")
// Start a coroutine
GlobalScope.launch {
delay(1000)
println("Hello")
}
Thread.sleep(2000) // wait for 2 seconds
println("Stop")
}
Start가 나타나고 1초있다 hello가 그리고 다시 1초있다(Start가 나오고 2초있다) Stop이 나타납니다.
여기서 코루틴은 launch함수 에서 시작한다고 했으니 그 안을 보면 delay로 1초를 주고 있습니다. 이 delay는 Thread.sleep과 다릅니다. Thread.sleep은 Thread 자체를 중단시키지만, delay는 Thread를 멈추지 않고 잠시 코루틴의 동작을 일지 정지 시킵니다. 코 루틴이 대기하는 동안이나 대기가 끝나을 때 Thread가 pool로 반환되면, 코루틴은 풀에서 사용할 수 있는 thread에서 재동작합니다. (이말은 코루틴이 아예 스레드랑 연관이 없는것은 아니라는 말이고, 꼭 main 스레드에서만 동작 시키는 것도 아니라는 말인것 같습니다.)
그리고 위의 코드에서 Thread.sleep을 빼면 Start가 나타나고 바로 Stop이 나타나고 프로그램이 종료될 것입니다. 그럼 hello는 어디갔냐?
코루틴이 동작하기전에 main Thread가 종료되어 코루틴도 같이 종료가 되버렸습니다. 그렇기 때문에 main Thread는 코루틴이 완료 될 때까지 기다려야 합니다.(코루틴을 반드시 사용한다면 말이죠)
만약 thread.sleep을 안쓰고 delay를 main Thread에 사로 사용하면 complie 에러가 발생할 것입니다. 왜냐하면 delay가 코루틴안에 있지 않기 때문 입니다. 하지만 다음과 같이 delay를 runBlocking{}으로 감싸주면 Thread.sleep처럼 코루틴이 동작하고 완료할 때까지 대기 할 수 있게 됩니다. (하지마 launch안에 delay를 3초로 하면 main Thread는 2초만 기다려주기 때문에 hello가 나타나지 않을 것 입니다.)
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main(){
println("Start")
// Start a coroutine
GlobalScope.launch {
delay(1500)
println("Hello")
}
runBlocking {
delay(2000)
}
println("Stop")
}
그래서 위 코드는 Start가 프린트 되고 launch{}를 통하여 코루틴이 동작하면, runBlocking을 통하여 다음 코루틴이 동작하면서 다른 코루틴이 완료 될떄까지 block합니다. 그 사이 hello가 나타나고 마지막에 stop이 프린트 되면서 main 함수는 종료 됩니다.
진짜 코루틴이 Thread에 비해 사용 비용이 저렴할까?
이번에는 정말로 코루틴이 thread에 비해 사용 비용이 저렴한지 알아보겠습니다.
2개의 코드를 비교해보겠습니다. 하나는 Thread를 하나는 코루틴을 사용하여 동일한 동작을 수행합니다
먼저 Thread 코드 입니다.
import java.util.concurrent.atomic.AtomicLong
import kotlin.concurrent.thread
fun main(){
val c = AtomicLong()
for (i in 1..1_000_000L)
thread(start = true) {
c.addAndGet(i)
}
println(c.get())
}
흠.. 사실 저는 위 코드를 돌리고 싶지 않습니다. 약 100만개의 Thread가 동작할껀데.. 음 안봐도 엄청 시간이 걸릴것 같습니다.
그럼 코루틴은 어떨까요?
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicLong
import kotlin.concurrent.thread
fun main(){
val c = AtomicLong()
for (i in 1..1_000_000L)
GlobalScope.launch {
c.addAndGet(i)
}
println(c.get())
}
위의 코드를 실행해보면 한 2초? 정도면 그 결과값으로 500000500000을 반환합니다.!
이 실험만봐도 Thread에 비해서 코루틴이 그 비용이 엄청 저렴하다는 것을 알 수 있습니다.!!
비동기 : 코루틴으로부터 값 받기
코루틴을 시작하는 다름 방법은 async{} 입니다. 이 방법은 launch{}와 같지만 , 코루틴의 결과를 반환하는 await() 함수를 가지는 Deferred<T> 값을 반환합니다. 이를 활용해 보겠습니다.
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
fun main(){
val deferred = (1..100).map { n ->
GlobalScope.async {
n
}
}
runBlocking {
val sum = deferred.map {
println(it.await().toLong())
it.await().toLong() }.sum()
print(sum)
}
}
위 코드는 1부터 100까지 값을 async{}로 코루틴에 적용합니다. 그럼 위의 설명처럼 Deffered<T>가 반환 됩니다. 그리고 runBlocking에서 it.await로 Deffered<T>로 반환되는 값들을 받고 sum을 하여 그 결과 값을 도출합니다. (그래서 위의 코드는 1부터 100까지 값이 나타나고 마지막에 그 sum한 값인 5050이 나타납니다. )
일시중단 기능
fun workload(n: Int): Int {
delay(1000)
return n
}
이 코드를 사용하면 delay에서 error가 나타납니다.
suspend fun workload(n: Int): Int {
delay(1000)
return n
}
하지만 이렇게!! fun 앞에 suspend를 붙여주면 그 함수에서 delay에 적용됩니다. 이렇게 만든 fun은 코루틴에서 호출되어 질 수 있습니다.(단, 코루인 밖의 다른곳에서는 호출 될 수 없습니다.) 그리고 위에서 사용한 delay await
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main(){
GlobalScope.launch {
var k = workload(100)
print(k)
}
runBlocking {
delay(2000)
}
}
suspend fun workload(n: Int): Int {
delay(1000)
return n
}
그래서 이렇게 작성하면 100이 프린트 됩니다!
참조
kotlinlang.org/docs/tutorials/coroutines/coroutines-basic-jvm.html
https://developer.android.com/kotlin/coroutines?hl=ko
https://brownbears.tistory.com/490
https://medium.com/@jooyunghan/%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%86%8C%EA%B0%9C-504cecc89407
'2023년 이전 > kotlin' 카테고리의 다른 글
코루틴(Coroutine) - Cancellation and Timeout (0) | 2020.09.13 |
---|---|
코루틴(Coroutine)이란 -2 (0) | 2020.09.13 |
kotlin - 널이 될수 있는 타입(?), 안전 호출 연산자(?.), 엘비스 연산자(?:), 안전 캐스트(as?) (0) | 2020.03.18 |
kotlin - 수신 객체 지정 람다 (0) | 2020.03.10 |
kotlin - 자바 함수형 인터페이스 활용 (2) | 2020.03.10 |