HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🤩
개발
/Kotlin/
Coroutine

Coroutine

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 실행
    • launch는 Job (참고)인스턴스를 반환하고, launch 내부에서 반환되는 값은 없음
    • async는 non-blocing future인 Deferred (참고) 를 반환하고 (promise) .await 를 통해 결과값을 받아낼 수 있음. 그러나 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") } }