Coroutine 이해하기 1
오늘은 Coroutine에 대해서 적어보려고 합니다.
분량이 너무 많고 적고 싶은 것도 많아서 한 페이지에 모두 다 적을 수는 없지만 그래도 최대한 깔끔하고 보기 편하게 이해하기 쉽게 적으려 노력 해보겠습니다..!
Android에서 비동기 프로그래밍과 병렬성 프로그래밍을 구현할때 RxJava, 과거엔 Async Task를 사용했습니다.
Coroutine에 대해서 공부해보니 RxJava랑은 좀 많이 다른 것 같더라구요.
그리고 많은 회사에서도 면접질문으로도 나오구요. 심지어 같이 일했던 팀장님께서도 하셨던 말씀중에 "개발자는 Thread 에서 한번 무너진다." 라고 말씀하셨던게 정말 기억이 나네요.
그때 그 시절에는 그만큼 어려운 개념이고 구현하기도 무척 까다로웠나 봅니다.
비동기 프로그래밍의 필요성은 제 블로그 가장 첫번째 글에 작성 하였으니 궁금하시다면 참고 부탁드립니다.
저는 Coroutine이 Kotlin 언어를 만들면서 생겨난 개념인 줄 알았는데, 그게 아니라고 합니다.
예전에 팀장님께서 말씀하시길 Coroutine 개념 자체는 나온지 오래 되었고 C++로 프로그래밍 하면서 경험해봤다고 말씀하셨습니다.
어느정도 역사가 있는 개념이니 저희 같이 차근차근히 알아보도록 합시다.(화이팅!!)
뭐 아무튼 오늘부터는 말씀드렸던 대로 Coroutine에 대해서 정리해 보겠습니다.
Coroutine을 구현하기 위해서는 CoroutineScope의 영역 내에서 실행 되어야 합니다.
CoroutineScope의 종류는 좀 나중에 알아보도록 하고 자세히 적기 전까지는 runBlocking 이라는 Scope를 사용하여 적어보겠습니다.
선행하기 앞서 Coroutine을 꼭 추가해주세요!(https://github.com/Kotlin/kotlinx.coroutines)
늘 그렇듯 Hello World 부터 찍고 시작해 볼까요??
fun main() = runBlocking {
println(Thread.currentThread().name)
println("Hello World")
}
실행결과
main @coroutine#1
Hello World
여기서 눈 여겨봐야할 점은 Thread.curreentThread().name 입니다. 이것을 찍은 이유는 나중에 Dispatcher 라는 개념이 나오는데 거기서 중요하기 때문입니다.
위에서 확인 할 수 있는 실행결과의 이유는 아래와 같습니다.
CoroutineScope 중 하나인 runBlocking 영역에서 (코루틴 스코프 영역에서) print를 찍었기 때문입니다.
fun main() = runBlocking {
launch {
println("launch: ${Thread.currentThread().name}")
println("World!")
}
println("runBlocking: ${Thread.currentThread().name}")
println("Hello")
}
실행결과
runBlocking: main @coroutine#1
Hello
launch: main @coroutine#2
World!
launch라는 용어가 나왔습니다. launch는 하나의 Coroutine을 실행하기 위한 영역 이라고 생각하셨으면 좋겠습니다.
마치
fun main() {
Thread {
/**/
}
}
처럼요.(이해를 돕기위해 예시를 든거지 launch가 Thread라는 말은 절대로 아닙니다.)
여기서 이상한 점이 있습니다.
분명 코드 실행상 launch 영역안에서의 print문이 먼저 찍힐 줄 알았는데 launch영역 밖의 코드가 먼저 실행이 되었습니다.
그 이유는 코루틴의 핵심은 Co + routine 입니다.
코루틴은 다른 코드와 자원을 함께한다는 특징이 있습니다.
제가(코드를 작성하는 개발자가) launch 영역을 먼저 수행하라고 작성하지 않았기 때문에 launch영역 밖의 코드가 우선권을 가지고 수행 후 launch 영역 내의 코드가 실행이 된겁니다. 즉, "코루틴의 핵심은 다른 코드와 자원을 함께 한다", "개발자가 어떤 코드에게 우선권을 줄 것인가." 가 핵심입니다. (runBlocking은 Hello를 출력하고서 바로 종료하지않고 launch 영역 내의 코드가 끝날때 까지 기다립니다.)
launch는 저희가 작성한 코드를 Queue에 넣어두고 다음 순서를 기다린다고 생각하시면 됩니다.
아직은 아리송하셔도 괜찮습니다. 계속 반복하면서 보시면 분명 이해하실 수 있습니다.
fun main() = runBlocking {
launch {
println("launch: ${Thread.currentThread().name}")
println("World!")
}
println("runBlocking: ${Thread.currentThread().name}")
delay(500L)
println("Hello")
}
실행결과
runBlocking: main @coroutine#1
launch: main @coroutine#2
World!
Hello
실행결과 보니 또 이상합니다.
"아니, 위에서는 launch 영역 밖의 코드가 먼저 우선이라고 해놓고 왜 또 launch영역 코드가 수행이 되는거지"
라고 생각 하실 수도 있습니다.
저렇게 실행결과가 나온 이유는 바로 delay 때문인데요.
혹시 Thread.sleep() 함수를 아시나요??
sleep 함수 처럼 delay 함수는 인자값만큼 잠깐 쉽니다. 0.5초만큼 쉬는 것지요.
또 말씀드리면 "코루틴의 핵심은 자원을 함께 사용한다." 입니다.
delay 하기전 print문이 실행되고 해당 영역이 잠깐 쉬는동안 자원 사용의 우선권은 launch 가 가져가게 됩니다.
때문에 launch영역의 코드가 실행되고 마저 남은 print()문이 실행되는거죠.
방금 작성한 예제에 하나만 바꿔보겠습니다.
fun main() = runBlocking {
launch {
println("launch: ${Thread.currentThread().name}")
println("World!")
}
println("runBlocking: ${Thread.currentThread().name}")
Thread.sleep(500)
println("Hello")
}
실행결과
runBlocking: main @coroutine#1
Hello
launch: main @coroutine#2
World!
여기 핵심적인 내용이 또 한가지 나옵니다..!(복잡하다면 잠깐 쉬고 다시 같이 공부해봐요,,)
프로세스는 운영체제로 부터, Thread는 프로세스로 부터 할당받습니다.
근데 delay함수와는 달리 Thead.sleep은 해당 Thread 자체를 (runBlocking 자체를) 인자값만큼 block하는거기 때문에 저런 결과가 나오는 겁니다.
fun main() = runBlocking {
launch {
println("launch1: ${Thread.currentThread().name}")
delay(1000L)
println("3!")
}
launch {
println("launch2: ${Thread.currentThread().name}")
println("1!")
}
println("runBlocking: ${Thread.currentThread().name}")
delay(500L)
println("2!")
}
실행결과
runBlocking: main @coroutine#1
launch1: main @coroutine#2
launch2: main @coroutine#3
1!
2!
3!
여기까지 잘 따라와 주셨다면 위 예제의 실행결과를 이해하 실 수 있으십니다.
1. runBlocking의 print문이 먼저 실행
2. delay 함으로써 자원 우선권을 넘김
3. 첫번째 launch가 자원 우선권을 받고 launch1 print문 실행
4. 첫번째 launch가 delay 함으로써 자원 우선권을 넘김
5. 두번째 launch가 자원 우선권을 받음으로써 launch2 print문과 1! print문을 실행
6. 위 코드가 모두 수행하고 남는 시간동안 대기
7. runBlocking의 delay가 첫번째 launch의 delay보다 짧기 때문에 우선권은 다시 runBlocking이 가짐으로써 2! 출력
8. 첫번째 launch의 delay가 끝난후 3! 출력
이번 포스팅의 핵심
1. Coroutine은 CoroutineScope 영역 내에서 실행된다.
2. launch 영역에 따라서 Coroutine이 수행된다.
3. Coroutine은 서로 자원의 우선권을 양보할 수가 있다.