https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/basics.md#coroutine-basics
Your first coroutine
-
fun main(){ GlobalScope.launch{ delay(1000L) println("World!") } println("Hello,") Thread.sleep(2000L) } // Hello, // World!
-
기본적으로 코루틴은 light-weight thread 이다.
CoroutineScope 라는 context 안에서 "launch" 와 같은 coroutine builder 를 통해 시작된다.
위의 예제에서는 GlobalScope 라는 coroutine 을 사용해서 launch 를 했다.
이 GlobalScope 는 전체 app 생명주기동안 살아있는 녀석이다.
-
만약 GlobalScope.launch { ... } 를 thread { ... } 로 바꾸면 컴파일러는 다음과 같은 에러를 내뱉는다.
Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function
delay 는 suspend function 이며 Thread.sleep 과는 다르게 thread 를 block 하지 않는다. 대신 coroutine 을 지연시킬 뿐이다.
-
suspend function 은 coroutine 이나 suspend function 에서만 호출되어야 한다는 규칙이 있다.
이 rule 은 기본 지식으로 외우면 된다.
Bridging blocking and non-blocking worlds
-
첫번째 예제는 non-blocking delay { ... } 와 blocking 인 Thread.sleep{ ... } 이 한 코드에 혼용되어 사용되었다.
이제 runBlocking coroutine builder 에 대해서 보자.
fun main(){ GlobalScope.launch{ delay(1000L) println("World!") } println("Hello,") runBlocking{ // block main thread delay(2000L) // JVM 이 살아있게 하기 위해 2초간 delay 한다. } } // Hello, // World
runBlocking 은 내부에 있는 coroutine 이 모두 끝날때까지 호출 thread 를 block 시킨다.
-
위 코드는 아래와 같이 쓰일 수도 있다.
fun main() = runBlocking<Unit> { GlobalScope.launch{ delay(1000L) println("World!") } println("Hello,") delay(2000L) } // Hello, // World!
-
다음은 suspending function 에 대한 unit test 코드이다.
class MyTest{ @Test fun testMySuspendingFunction() = runBlocking<Unit>{ // ... } }
Waiting for a job
-
다른 coroutine 의 동작이 끝나기를 delay 로 기다리는 것은 좋은 방법이 아니다.
Job 이라는 녀석을 이용해서 명시적으로 기다릴 수 있다.
fun main() = runBlocking{ val job = GlobalScope.launch{ delay(1000L) println("World!") } println("Hello,") job.join() } // Hello, // World!
위 코드는 delay 와 동일한 결과를 냈지만, 시간에 구애받지 않는 더 좋은 코드가 되었다.
Structured concurrency
-
GlobalScope.launch 를 사용하게 되면 우리는 top-level coroutine 을 생성하는 것이다.
비롯 light-weight 이기는 하나 그래도 여전히 memory 를 비롯한 res 를 사용한다.
우리가 만약 새롭게 launch 된 coroutine 이 돌고 있다는 사실을 잊는다면, 그 로직은 계속 돌 것이다.
이런 것들이 반복되면 의미없는 코드가 돌거나, 너무 많은 코루틴의 돌아 out of memory 현상이 발생할 수도 있다.
수동으로 모든 launch 된 coroutine 을 참조하고, join 으로 끝나기를 기다리는 것은 에러 발생 가능성도 높다.
그래서 더 좋은 방법을 생각해냈다.
우리는 코드에 구조화된 동기화를 할 수 있다.
GlobalScope 에서 launch 하는 대신, 우리가 threads 를 사용하듯이 우리가 사용하는 context 범위 내에서 coroutine 을 특정한 scope 안에서 launch 하는 것이다.
우리의 예제에서는 main function 이 runBlocking coroutine builder 를 사용하였다.
runBlocking 을 비롯한 모든 coroutine builder 은 code block 에 "CoroutineScope" instance 를 추가한다.
우리는 이 scope 에서 join 을 명시적으로 부를 필요 없이 coroutine 을 launch 시킬 수 있다.
왜냐하면 바깥쪽 coroutine (예제에서는 runBlocking) 은 해당 scope 에서 launch 된 coroutine 이 모두 종료되기 전까지 종료되지 않기 때문이다.
그래서 우리는 예문을 다음과 같이 변경시킬 수 있다.
fun main() = runBlocking{ launch { // launch new coroutine in the scope of runBlocking delay(1000L) println("World!") } println("Hello,") } // Hello, // World!
Scope builder
-
다른 builder 에 의해 제공되는 coroutine scope 이외에 우리만의 scope 을 "coroutineScope" 이라는 builder 를 통해 만들 수도 있다.
이것은 새로운 coroutine scope 를 만들며 이 scope 에서 launch 된 녀석들이 모두 끝날 때까지 끝나지 않는다.
runBlocking 과 coroutineScope 의 차이점은, coroutineScope 은 현재 thread 를 blocking 하지 않는다는 것이다.
fun main() = runBlocking{ launch{ delay(200L) println("Task from runBlocking") } coroutineScope{ launch{ delay(500L) println("Task from nested launch") } delay(100L) println("Task from coroutine scope") } println("Coroutine scope is over") } // Task from coroutine scope // Task from runBlocking // Task from nested launch // Coroutine scope is over
-
돼왕 : Coroutine scope is over 가 왜 마지막에 찍히는지 정확히 이해되지 않는다면 다음 글을 보자.
[coroutine] coroutineScope 의 동작 특성을 알아보자.
Extract function refactoring
-
launch{ ... } 코드를 다른 function 으로 분리해내보자.
분리해낸 함수는 suspend modifier 를 붙여야 하며, 이것을 suspending function 이라 부른다.
suspending function 은 coroutine 에서 일반 function 처럼 이용될 수 있으며, 이 안에서는 delay 와 같은 다른 suspending function 을 부를 수 있다.
fun main() = runBlocking { launch{ doWorld() } println("Hello,") } suspend fun doWorld() { delay(1000L) println("World!") } // Hello, // World!
-
현재 scope 에서 불리는 coroutine builder 를 가진 함수를 extract 할때는 어떻게 해야 할까?
이 경우에는 suspend modifier 로는 충분하지 않다.
CoroutineScope 의 extension method 로 doWorld 를 만드는 것이 한가지 방법이다. 그러나 이 방법은 API 를 깔끔하게 만들지도 않고 언제나 적용할 수 있는 방법도 아니다.
주로 사용하는 방법은 해당 함수에서 명시적으로 접근할 수 있는 CoroutineScope 를 class 에 field 로 갖게 하거나, outer class 가 CoroutineScope 를 구현한 것을 접근할 수 있게 하는 것이다.
마지막 방법으로 CoroutineScope(coroutineContext) 를 사용할 수 있지만, 이 방법은 구조적으로 안전하지 않다. 왜냐면 이 함수의 실행 scope 를 더 이상 control 할 수 없기 때문이다. 오직 private API 만 이 builder 를 사용할 수 있다.
( 돼왕 : 정확한 이해는 나중에 실 사용하거나, 추후 tutorial 들을 모두 봐야 이해가 좀 가능할 것이다. )
Coroutines ARE light-weight
-
fun main() = runBlocking { repeat(10_0000){ launch{ delay(1000L) printl(".") } } }
이 코드를 실행하면 1초마다 10만개의 coroutine 을 launch 하며, 각 coroutine 은 점을 찍는다.
이것을 thread 로 시도해보면, 무슨 일이 생길까? ( 대부분 out of memory error 가 발생할 것이다. )
Global coroutines are like daemon threads
-
다음 코드는 GlobalScope 에서 장수하는 coroutine 을 launch 하며, "I'm sleeping" 을 1초에 2번 프린트한다.
main 은 GlobalScope launch 후, delay 후 return 한다.
fun main() = runBlocking{ GlobalScope.launch{ repeat(1000){ i -> println("I'm sleeping $i ...") delay(500L) } } delay(1300L) } // I'm sleeping 0 ... // I'm sleeping 1 ... // I'm sleeping 2 ...
runBlocking 이 1300L ms 후에 종료되면서 app 이 종료되었기 때문에, GlobalScope 의 로직도 종료된다.
GlobalScope 에서 launch 된 active coroutine 은 process 를 alive 하는 역할을 하지 않는다.
데몬 스레드처럼 일반 쓰레드(main 등)이 모두 종료되면 함께 종료된다.
'프로그래밍 놀이터 > Kotlin, Coroutine' 카테고리의 다른 글
[coroutine] Cancellation and Timeouts ( 취소와 타임아웃 ) (0) | 2020.03.09 |
---|---|
[coroutine] coroutineScope 의 동작 특성을 알아보자. (0) | 2020.03.08 |
Coroutine 과 놀아보기 #2 (2) | 2019.05.23 |
Coroutine 과 놀아보기 #1 (0) | 2019.05.22 |
Coroutine first launch slow.. (0) | 2019.04.26 |
댓글