BasicsAn explicit jobCoroutines are light-weightCoroutine 실행Coroutine context and dispatchersCoroutine DispatcherCoroutineContextJob in the contextContext element 결합하기Coroutine ScopeThread-local dataDebuggingExceptions HandlingException Propagation
Basics
coroutine은 정지가능한 연산의 인스턴스이다. 코루틴은 개념적으로는 스레드와 비슷한데, 코드블럭을 갖고있고 그 코드블럭을 다른 코드들과 동시적으로 실행한다는 점에서 그렇다. 그러나 코루틴은 특정 스레드에 묶여있지 않고 한 스레드에서 실행을 중지시키고 다른 스레드에서 실행을 재개할 수 있다.
fun main() = runBlocking { // this: CoroutineScope launch { // launch a new coroutine and continue delay(1000L) // non-blocking delay for 1 second (default time unit is ms) println("World!") // print after delay } println("Hello") // main coroutine continues while a previous one is delayed }
launch
: coroutine builder. 다른 코드와 함께 새로운 코루틴을 concurrent 하게 시작함(독립적으로 동작하게 됨)
delay
: suspending function. coroutine을 명시한 시간만큼 정지시켜줌. coroutine을 suspend 하는 것은 실행되고 있던 스레드를 block 하지 않고 다른 코루틴이 그 스레드를 사용할 수 있음
runBlocking
: 또한 coroutine builder 로서 일반적인 함수인 fun main() 과 coroutine 사이를 연결해주는 역할을 함.runBlocking
이 없으면launch
call을 할 수 없음.launch
는CoroutineScope
안에서만 사용 가능함- runBlocking은 이를 실행시키는 스레드(위에서는 main 스레드)가 runBlocking을 호출하는 동안(runBlocking 내부의 모든 coroutine이 실행을 완료하기 까지) block 된다는 것을 의미
- application의 top-level에서는 많이 쓰는 것을 볼 수 있지만 실제 코드 안에서는 많이 사용안함. 스레드는 비싼 자원이기에
An explicit job
val job = launch { // launch a new coroutine and keep a reference to its Job delay(1000L) println("World!") } println("Hello") job.join() // wait until child coroutine completes println("Done")
Coroutines are light-weight
import kotlinx.coroutines.* fun main() = runBlocking { repeat(50_000) { // launch a lot of coroutines launch { delay(5000L) print(".") } } }
import kotlin.concurrent.* fun main() = repeat(50_000) { thread { Thread.sleep(5000L) print(".") } }
- 아래 코드가 위의 코드보다 더 많은 메모리를 잡아 먹게 됨.
Coroutine 실행
- CoroutineScope를 생성
- CoroutineScope() 정적함수 호출 → 실행의 흐름을 방해하지 않음. 따로 block 되지 않음
- runBlocking() 을 통해 coroutineScope 생성 → block 이 됨
- CoroutineScope 안에서
launch
나async
를 통해 coroutine 실행
val time = measureTimeMillis { val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } println("The answer is ${one.await() + two.await()}") } println("Completed in $time ms")
async 를 lazily 하게 start 하는 것도 가능 (start 를 호출하거나 await 를 호출할 때 coroutine을 시작함)
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")
Coroutine context and dispatchers
코루틴은 항상 CoroutineContext 타입의 값으로 대표되는 어떠한 context 안에서 실행된다.
코루틴 context는 다양한 요소의 집합으로 이루어져 있는데, 메인 요소는 코루틴의
Job
이고, 그리고 그것의 dispatcher
임Coroutine Dispatcher
어떤 스레드 혹은 스레드들이 해당 coroutine 의 실행을 담당할지를 결정하는 것이 Coroutine Dispatcher가 하는 역할임
Coroutine Dispatcher는 코루틴의 실행을 특정 스레드에 할당할수도, 스레드 풀에 할당할 수도, 한정짓지 않고 실행되게 할 수도 있음
모든 Coroutine builder(
launch
, async
) 가 optional 한 CoroutineContext 값을 받는데 이 값이 dispatcher를 명시하는데에 사용됨launch { // context of the parent, main runBlocking coroutine println("main runBlocking : I'm working in thread ${Thread.currentThread().name}") } launch(Dispatchers.Unconfined) { // not confined -- will work with main thread println("Unconfined : I'm working in thread ${Thread.currentThread().name}") } launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher println("Default : I'm working in thread ${Thread.currentThread().name}") } launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}") } Unconfined : I'm working in thread main Default : I'm working in thread DefaultDispatcher-worker-1 newSingleThreadContext: I'm working in thread MyOwnThread main runBlocking : I'm working in thread main
- launch가 파라미터 없이 실행되면 실행된 CoroutineScope의 context를 물려받음
Default
: 공유된 백그라운드의 스레드 풀을 이용
newSingleThreadContext
: 코루틴이 실행될 스레드를 생성. dedicated 된 스레드는 비싼 자원이기에 실제 어플리케이션에서는 해당 스레드는 필요없어지면 꼭 할당을 해제해주어야 함
Unconfined
: 처음에는 caller thread에서 코루틴을 시작. 근데 중지되고 나서 다시 실행될 때는 호출된 suspending 에 의해 실행되는 thread 가 정해진다. 이 dispatcher 타입은 CPU time 을 많이 소모하지 않거나 특정 스레드에 한정된 공유 데이터를 업데이트 해야하는 경우가 아닐때 적합함
기본적으로 Dispatcher는 바깥의 CoroutineScope에서 사용하는 것을 그대로 물려받음
runBlocking { launch(Dispatchers.Unconfined) { // not confined -- will work with main thread println("Unconfined : I'm working in thread ${Thread.currentThread().name}") delay(500) println("Unconfined : After delay in thread ${Thread.currentThread().name}") } launch { // context of the parent, main runBlocking coroutine println("main runBlocking: I'm working in thread ${Thread.currentThread().name}") delay(1000) println("main runBlocking: After delay in thread ${Thread.currentThread().name}") } }
Unconfined : I'm working in thread main main runBlocking: I'm working in thread main Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor main runBlocking: After delay in thread main
CoroutineContext
Job in the context
println("My job is ${coroutineContext[Job]}")
코루틴의 Job은 context의 일부분으로써, 위의 표현식을 통해 조회가 가능함
Context element 결합하기
launch(Dispatchers.Default + CoroutineName("test")) { println("I'm working in thread ${Thread.currentThread().name}") }
- + 연산자로 coroutine context에 여러 요소들을 정의할 수 있음
- dispatcher
- courinteName
- coroutineExceptionHandler
Coroutine Scope
CoroutineScope는 그 안에서 실행된 모든 coroutine을 한번에 cancel 할 수 있게 감싸주는 역할을 함(memory leak 이 발생하지 않도록)
CoroutineScope 인스턴스는 CoroutineScope() 나 MainScope() 정적 함수들에 의해 생성될 수 있음
- CoroutineScope 는 일반적인 목적의 scope 이고 MainScope()는 UI 어플리케이션에 대한 scope를 만듦
class Activity { private val mainScope = MainScope() fun destroy() { mainScope.cancel() } fun doSomething() { // launch ten coroutines for a demo, each working for a different time repeat(10) { i -> mainScope.launch { delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc println("Coroutine $i is done") } } } } // class Activity ends val activity = Activity() activity.doSomething() // run test function println("Launched coroutines") delay(500L) // delay for half a second println("Destroying activity!") activity.destroy() // cancels all coroutines delay(1000) // visually confirm that they don't work // Output Launched coroutines Coroutine 0 is done Coroutine 1 is done Destroying activity!
Thread-local data
thread local 데이터를 코루틴 간에 전달해서 계속적으로 공유하게 하는 것은 때때로 편리하다
ThreadLocal의 asContextElement 확장 함수를 이용하면 coroutine이 thread 를 변경했을때마다 그 값을 그대로 불러와 준다.
그러나 한가지 주요 한계점은 thread local의 값이 바뀌면 새로운 값은 전파가 되지 않음. 다른 coroutine으로 갈때 변경된 값이 아닌 처음에 셋팅한 값으로 불러와짐. thread-local의 값을 코루틴 안에서 변경하려면
withContext
함수를 사용하라.threadLocal.set("main") println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'") val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) { println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'") yield() println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'") } job.join() println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'") /// Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main' Launch start, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch' After yield, current thread: Thread[DefaultDispatcher-worker-2 @coroutine#2,5,main], thread local value: 'launch' Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Debugging
- Intellij에서 Debugging 가능
- Run the following code with
-Dkotlinx.coroutines.debug
JVM option:
val a = async { log("I'm computing a piece of the answer") 6 } val b = async { log("I'm computing another piece of the answer") 7 } log("The answer is ${a.await() * b.await()}") [main @coroutine#2] I'm computing a piece of the answer [main @coroutine#3] I'm computing another piece of the answer [main @coroutine#1] The answer is 42 // [실행되고 있는 thread coroutine 이름(고유하게 할당됨)] log("Started main coroutine") // run two background value computations val v1 = async(CoroutineName("v1coroutine")) { delay(500) log("Computing v1") 6 } val v2 = async(CoroutineName("v2coroutine")) { delay(1000) log("Computing v2") 7 } log("The answer for v1 * v2 = ${v1.await() * v2.await()}") // 위와같이 CoroutineName을 정해주면 코루틴의 이름이 명시된 대로 프린트됨 [main @main#1] Started main coroutine [main @v1coroutine#2] Computing v1 [main @v2coroutine#3] Computing v2 [main @main#1] The answer for v1 * v2 = 42
Exceptions Handling
Exception Propagation
- launch : exception 을 그냥 uncaught exception 으로 처리함
- async, produce : user가 final exception 을 consume 할지에 따라 달라짐(await 나 receive를 호출하면)
@OptIn(DelicateCoroutinesApi::class) fun main() = runBlocking { val job = GlobalScope.launch { // root coroutine with launch println("Throwing exception from launch") throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler } job.join() // 이걸 try catch 로 묶어도 exception 안잡힘. 이미 launch 하면서 coroutine 끝난 상태이기에. 그치만 join 을 함으로써 exception 이 propagation 됨 println("Joined failed job") val deferred = GlobalScope.async { // root coroutine with async println("Throwing exception from async") throw ArithmeticException() // Nothing is printed, relying on user to call await } try { deferred.await() println("Unreached") } catch (e: ArithmeticException) { println("Caught ArithmeticException") } }