kotlin Coroutine 정리(4, Coroutine Context and Dispatchers)

Coroutine Context and Dispatchers.

코루틴은 항상 어떠한 context 내에서 실행이 되는데, 이러한 context는 CoroutineContext type의 값에 의해 표현(represented)된다(이러한 타입은 kotlin standard library에 정의 되어 있음).

 

Coroutine context는 다양한 요소(elements)의 집합이다. 주요한 요소는 coroutine의 <1>Job이고(이전 장에 잠시 다룬 것이다), Job의 <2>dispatcher이다. dispatcher은 이번 포스팅에서 자세히 다룬다.

 

먼저, 이번 페이지에서 살펴볼 목차는 아래와 같다(kotlinlang.org/docs/reference/coroutines/coroutine-context-and-dispatchers.html)

 

Coroutine Context and Dispatchers

 

Dispatchers and threads

 

1> coroutine context는 coroutine dispatcher (see CoroutineDispatcher)를 포함하는데 이는 코루틴에 대응하는 어떤 쓰레드(들)가 execution에 있어서 사용되는지를 결정한다. coroutine dispatcher는 특별한 쓰레드에 대한 <1>실행을 제한하거나, 그 실행을 <2>쓰레드 풀에 보내거나(dispatch), <3>제한 없이 실행하도록 만들 수 있다. 

 

2> launch나 async와 같은 모든 코루틴 빌더는 optional CoroutineContext 파라미터를 받아들이고 명시적으로(explicitly) 새로운 코루틴이나 다른 context 요소에 대한 dispatcher를 상세화(sepcify) 할 수 있다.

 

[그림]

 

3> launch{ ... }를 파라미터 없이 사용할 때, 그것은  (launched되는 주체인 CoroutineScope로부터의) context를 상속한다(따라서 dispatcher 또한 당연히 상속한다). 이 경우에, 그것(= launch{...})은 메인쓰레드에서 동작하는 main runBlocking  coroutine 의 context를 상속한다.

 

4> Dispatcher.Unconfined 는 특별한 dispatcher로서 메인쓰레드에서 run을 하기 위해서 등장하지만, 실제로는 이에대한 메커니즘은 다른 dispatcher는 다르다. 이는 추후에 다시 다룬다.

 

5>코루틴들이 GlobalScope에서 launched 될 때, 사용되는 기본(default) dispatcher는 Dispatchers.Default로 표시되며 스레드의 공유 백그라운드 풀을 사용한다. 또한 launch(Dispatcher.Default){}를 사용하여 Global.launch {} 와 같은 dispatcher를 사용할 수 있다.

 

6> newSingleThreadContext는 코루틴 동작을 위한  a thread를 만든다. dedicaterd 된 thread는 아주 비싼 자원이다.

 실제 어플리케이션에서 dedicated된 single Thread가 필요없어진 경우, 반드시 close function을 사용하여  release를 시켜주면되는데, 탑레벨 변수에 저장하고 나 어플리케이션 전반에 있어 재사용하거나.... 하는식으로 일을 진행한다.

 

 

Unconfined vs confined Dispatcher

[The Dispatchers.Unconfined 코루틴 dispatcher]는 호출 쓰레드 내에서 코루틴을 start한다. 한데 여기서 얘는 first suspension point까지만 start한다. suspension 이후, invoke되는 suspending function에 의해 fully determined되는 쓰레드 내에서 resume된다. a> CPU time을 consume하지 않고, 또한 b> 어떠한 특별한 쓰레드에 의해 제한된 shared Data(like UI)를 업데이트 하지 않는 코루틴의 경우에는 the unconfined dispatcher를 사용하는게 좋다.

반면에 dispatcher는 기본적으로 외부 CoroutineScope에서 상속된다. 특히, runBlocking coroutine을 위한 default dispatcher는 invoker thread에 의해 제한되어있으므로, 이를 상속하게 되면 예측가능한 FIFO 스케줄링이 되는 쓰레드로 해당 dispatcher를 confining(제한)할 수 있다.

 

[그림]

 

 

위 그림의 결과를 보면, runBlocking{ }으로 부터 상속 받은 context를 가진 코루틴은 연속적으로 main thread에서 실행되고 있으나, unconfined 된 코루틴의 경우에는 delay function이 사용되는 default executor 쓰레드에 속해 있음을 볼 수 있다

 

[제한되지 않은 디스패처는 코루틴의 일부 작업을 즉시 수행해야하므로 나중에 실행하기 위해 코루틴을 디스패치 할 필요가 없거나 바람직하지 않은 부작용이 발생하는 특정 코너 경우에 도움이 될 수있는 고급 메커니즘이다. The unconfined dispatcher는 일반적인 코드에서는 사용하여서는 안될 것이다.( 구글 번역..)

The unconfined dispatcher is an advanced mechanism that can be helpful in certain corner cases where dispatching of a coroutine for its execution later is not needed or produces undesirable side-effects, because some operation in a coroutine must be performed right away. The unconfined dispatcher should not be used in general code]

 

Debugging coroutines and threads

코루틴들은 하나의 쓰레드에서 suspend할 수 있으며, 또 다른 Thread에서 이를 resume 할 수 있다. 따라서 single-threaded dispatcher라고 할 지라도, 그것에서 어떤 코루틴이 무엇을 하는지 어디에 있는지 언제하는지에 대하여 (사용자가 특별한 툴링 기법을 가지고 있지 않는 한) 알기가 어렵다.

 

Coroutines can suspend on one thread and resume on another thread. Even with a single-threaded dispatcher it might be hard to figure out what the coroutine was doing, where, and when if you don't have special tooling

 

Debugging with IDEA

 

인텔리제이 아이디어에서 코루틴 디버깅을 간단히 한 플러그인인 Coroutine Debugger가 있다.

여기서 보면 서스펜디드된 코루틴과 동작중인 현재 코루틴에 대한 정보를 Debug Tool Window에 있는 Coroutine Tab에서 확인할 수 있다. 코루틴들은 그들을 동작시키고 있는 Dispatcher에 의해 Group화 되어있다.

 

[그림]

 

이 디버거의 기능으로는 3가지.

1> 각각의 코루틴 상태를 쉽게 체크할 수 있다.

2>  local과 capture된 변수를 확인 할 수 있다(in suspeded coroutine, and running)

3> 모든 코루틴 creation stack을 볼 수 있고, 그 내부또한 볼 수 있다. The stack은 모든 프레임에서 variable 변수들을 포함하고 있으며, 심지어 디버깅 도중 사라지는 것들 또한 stack에서 볼 수 있다.

 

사용자가 full report를 보고 싶다면 CoroutineTab에서 Get Coroutine Dump를 사용하여 Dumping된 데이터를 얻을 수 있다.

 

Learn more about debugging coroutines in this blog post and IntelliJ IDEA documentation.

 

 

Debugging using logging

 

Coroutine Debugger를 사용하지 않고 쓰레드에서 duggling Application에 접근하는 또다른 방법은 각각의 log statement에 log file 안에 thread name을 적는 것이다. 이 특징은 logging framework에 의해 universally supported 되고 있다. 코루틴을 사용할때 쓰레드이름은 context에 대한 많은 정보를 주지않는다. 따라서 kotlinx.coroutines를 쓰면 더 많은 정보를 더 쉽게 용이하게 얻을 수 있다.

 

아래를 -Dkotlinx.coroutines.debug JVM option으로 실행시켜보자.

[그림]

 

위의 예제는 코루틴이 3개 존재한다. 메인 코루틴 #1은 runBlocking 안에 속해 있으며 나머지 두 코루틴은 a(#2)와 b(#3)라는 deferred value를 연산한다. 그것들은 모두 runBlocking의 context안에 executing되고, main Thread안에서 confined된다. output Code는 아래와 같다.

 

[연산결과 그림]

 

log 함수는 사각 [ ]  blanket인 여기에다가 쓰레드의 이름을 출력하고, 사용자는 그것에 appended되어 있는 현재 실행중인 코루틴의 identifier와 함께 메인 쓰레드를 볼 수 있다. 이 identifier는 디버깅 모드가 켜져있을 때 생성 된 모든 코 루틴에 연속적으로 할당된다.

Jumping between threads

 [그림]

-Dkotlinx.coroutines.debug 옵션을 사용하여 위 코드를 실행해본다.

이는 여러 새로운 기능들을 증명한다. 하나는 explicitly specified context를 포함하는 runBlocking을 사용하는 것이고 또 다른 하나는 withContext함수를 써서 같은 코루틴에 머물면서 coroutine context가 바뀌는 것이다.

이 예제에서는 use 함수를 사용하는데, 스탠다드 코틀린에서는 newSingleThreadContext에 의해 생성된 쓰레드가 더 이상 필요가 없을 경우 자동으로 해제해준다.

 

Job in the context

코루틴의 Job은 그 Context의 부분이다. 그리고 coroutineContext[job]을 사용하여 그것을 retrieve할 수 있다.

[그림]

 

Parental Responsibilities

 

부모의 의무. 

부모 코루틴은 항상 그의 모든 자식들이 completion하기전까지는 항상 wait한다.

부모는 children을 launch하는 모든 외향적인 track을 가지고 있을 필요는 없다. 또한 끝에서 그들을 기다리기 위해서 Join.join을 사용하여 기다릴 필요도없다.

 

 

Naming coroutines for debugging

 

 

Combining context elements

Coroutine scope

Thread-local data

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유