코루틴 디스패처 (온라인 슬롯)
코틀린 코루틴 (10)
코루틴 디스패처 (온라인 슬롯)
코루틴 취소에서 다룬 예제들은 launch의 파라미터로 온라인 슬롯s.Default라는 것을 넘겨주고 있다. 그에 대해서는 일단 고려하지 않았으나 사실 정확한 이해를 위해서는 해당 부분도 무시할 수 없다. 온라인 슬롯s.Default는 코루틴 디스패처의 하나이다. 그러면 코루틴 디스패처가 무엇인지 또 의문이 생긴다. 코루틴 디스패처는 CoroutineContext의 요소(Element) 중 하나이다.
그러면 CoroutineContext는 뭘까? CoroutineContext는 코루틴이 실행되는 환경에 대한 요소들의 집합이다. 이미 보았던 Job이 바로 CoroutineContext의 요소 중 하나이다. 그리고 코루틴 디스패처 또한 CoroutineContext의 요소인 것이다.
CoroutineContext는 CoroutineContext.kt 파일에 정의되어 있는 인터페이스이다. 해당 인터페이스는 get(), fold(), plus(), minusKey()라는 4개의 메서드를 가진다. 그중에서 get()은 파라미터에 주어지는 key에 해당하는 컨텍스트를 반환하는 메서드이다. key라는 파라미터의 타입은 Key이다. Key 역시 인터페이스이고 제네릭 타입 파라미터는 Element 타입이다.
Element는 CoroutineContext를 상속하는 인터페이스이다. key를 멤버 속성으로 가지고 해당 key를 기반으로 CoroutineContext에 등록된다. Element의 예로 CoroutineId, CoroutineName, Coroutine온라인 슬롯, CoroutineScope, Job 등이 있고 각 요소들은 고유의 key를 기반으로 CoroutineContext에 등록되는 것이다.
여기까지 이해를 했다면 다음 예제를 보고 launch의 파라미터로 코루틴 디스패처 온라인 슬롯s.Default가 전달 가능한 이유를 알 수 있을 것이다. 먼저 launch의 첫 번째 인자(=context)의 타입이 무엇인지 알아야 한다.
사실 이전에코루틴 스코프를 이야기할 때 launch를 살펴본 적이 있다. 다시 보면 다음과 같다.
context의 타입이 CoroutineContext이다. 온라인 슬롯s.Default는 코루틴 디스패처이다. 코루틴 디스패처는 CoroutineContext의 Element 중 하나이다. Element는 CoroutineContext를 상속하는 인터페이스이다. 따라서 온라인 슬롯s.Default는 CoroutineContext 타입인 context로 전달될 수 있는 것이다.
그러면 CoroutineContext의 Element 중 하나인 코루틴 디스패처는 도대체 무엇을 하는 녀석일까?코루틴 디스패처는 코루틴이 실행될 스레드 또는 스레드풀을 결정하는 역할을 한다.launch 뿐만 아니라 모든 코루틴 빌더들은 CoroutineContext을 옵셔널 파라미터로 갖는다. 따라서 코루틴 빌더를 통해 새로운 코루틴을 만들 때 코루틴 디스패처는 물론이고 다른 CoroutineContext 요소들도 지정할 수 있다. 일단 여기서는 코루틴 디스패처에 집중한다.
코루틴 디스패처는 특정 목적에 맞게 최적화된 다양한 종류가 존재한다. 주요 디스패처의 종류는 다음과 같다.
온라인 슬롯s.IO: I/O 작업에 최적화된 디스패처다. 파일 읽기/쓰기, 네트워크 요청 등 블로킹 I/O 작업에 사용된다. 일반적으로CPU 코어 수보다 많은 스레드를 처리할 수 있다.
온라인 슬롯s.Unconfined: 코루틴을 호출한 스레드에서 시작하고, 첫 번째 중단 지점까지 실행한다. 이후에는 중단된 지점에서 재개될 때 사용 가능한 스레드에서 실행된다. 이는 특정 스레드에 바인딩되지 않으며, 주로 테스트나 특정 상황에서만 사용된다.
온라인 슬롯s.Default: CPU 집약적인 작업에 최적화된 디스패처이다. 일반적으로 CPU 코어 수와 동일한수의 스레드를 사용한다. 하지만필요 시 증감한다.복잡한 계산이나 데이터 처리 작업에 사용된다.
newSingleThreadContext: 새로운 단일 스레드를 생성하여 코루틴을 실행한다. 특정 작업을 하나의 스레드에서 순차적으로 실행해야 할 때 사용된다.
각 디스패처가 실제로 어떤 스레드에서 실행되는지 확인하는 방법은 간단하다.
실행 결과는 다음과 같다.
Unconfined : thread main
IO : thread 온라인 슬롯1
Default : thread 온라인 슬롯1
main runBlocking : thread main
newSingleThreadContext: thread MyThread
특정 디스패처를 지정하지 않은 launch { }의 결과가 thread main인 이유는 실행된 코루틴 스코프로부터 context를 상속받기 때문이다. 즉 부모 코루틴의context를 상속받는다. 이 경우에 부모 코루틴은 runBlocking 코루틴이고 기본적으로 메인 스레드에서 실행된다.
온라인 슬롯s.IO와 온라인 슬롯s.Default는 동일한 스레드풀을 공유한다. 따라서 실행 결과를 보면 둘 다 DefaultDispatcher-worker-1스레드에서 실행된 것을 확인할 수 있다. 그러면 왜 굳이 둘을 구분하는지 의문이 생길 수 있다. 온라인 슬롯s.IO는 I/O 작업에 최적화되어 더 많은 스레드를 사용할 수 있도록 설계되어 있다.
실제로 그렇게 동작하는지 확인하기 위해서 아래와 같은 코드를 작성해 볼 수 있다.
실행 결과는 다음과 같다. (실행할 때마다 결과가 다르다는 것은 알고 있으리라 믿고 추후에 따로 언급하지 않는다.)
IO threads: 44
Default threads: 11
IO thread names: [온라인 슬롯1, 온라인 슬롯3, 온라인 슬롯2, 온라인 슬롯7, 온라인 슬롯5, 온라인 슬롯6, 온라인 슬롯10, 온라인 슬롯9, 온라인 슬롯4, 온라인 슬롯11, 온라인 슬롯12, 온라인 슬롯8, 온라인 슬롯15, 온라인 슬롯14, 온라인 슬롯18, 온라인 슬롯19, 온라인 슬롯22, 온라인 슬롯16, 온라인 슬롯13, 온라인 슬롯23, 온라인 슬롯17, 온라인 슬롯20, 온라인 슬롯21, 온라인 슬롯28, 온라인 슬롯24, 온라인 슬롯31, 온라인 슬롯30, 온라인 슬롯29, 온라인 슬롯25, 온라인 슬롯34, 온라인 슬롯32, 온라인 슬롯27, 온라인 슬롯37, 온라인 슬롯33, 온라인 슬롯38, 온라인 슬롯40, 온라인 슬롯35, 온라인 슬롯36, 온라인 슬롯43, 온라인 슬롯42, 온라인 슬롯44, 온라인 슬롯26, 온라인 슬롯41, 온라인 슬롯39]
Default thread names: [온라인 슬롯45, 온라인 슬롯25, 온라인 슬롯15, 온라인 슬롯11, 온라인 슬롯21, 온라인 슬롯14, 온라인 슬롯5, 온라인 슬롯38, 온라인 슬롯22, 온라인 슬롯12, 온라인 슬롯29]
온라인 슬롯s.IO에서 확실히 더 많은 스레드를 사용한 것을 실행 결과로 직접 확인할 수 있다.
이렇게 코루틴 디스패처가 코루틴이 실행될 스레드 또는 스레드풀을 결정하는 것을 확인했다. 그러면 이제 앞서 언급했던 내용 중에서 어쩌면 이해하지 못했을 내용에 대한 퍼즐을 맞출 수 있다.
코루틴(Coroutine) 생성 및 실행에서 코루틴은 특정한 스레드에 종속되지 않고 일시정지를 통해 동시성을 제공한다고 했다.
코루틴 delay 함수에서 다시 한번 코루틴은 특정한 스레드에 종속되지 않는다고 강조했다. 또한 다른 스레드(또는 스레드풀)에서 실행되는 launch가 두 개라면 실행 순서를 보장할 수 없다고 했다. 그리고 이 내용이 이해되지 않으면 그냥 넘어가고 추후 온라인 슬롯라는 개념을 다룰 때 다시 이야기하게 될 것이라고 했다.
코루틴 취소에서 온라인 슬롯s.Default를 사용했을 때 cancel()이 호출되기 전에 launch 블록이 실행될 가능성이 높다고 했다.
(다 계획이 있었죠?)
그럼하나씩확인해 보자.
코루틴이 특정한 스레드에 속하지 않는다는 것은 좀 더 명확히 보려면 다음과 같은 JVM 옵션을 주면 된다.
-Dkotlinx.coroutines.debug
인텔리제이를 사용한다면 아래와 같은 경로에 추가하면 된다.
이렇게 하면 실행 결과에서 스레드 이름에 코루틴 정보가 추가된다. 예를 들면 다음과 같이 보인다.
온라인 슬롯2 @coroutine#3
@coroutine#3라는 코루틴 정보가 추가된 것을 확인할 수 있다.
1) 코루틴은 특정한 스레드에 종속되지 않는다.
그럼 실제로 동일한 코루틴이 특정 스레드에 종속되지 않고 동시성을 가지는 예제 코드를 보자.
실행 결과는 다음과 같다.
IO threads: 3
Default threads: 3
IO thread names: [온라인 슬롯2 @coroutine#2, 온라인 슬롯1 @coroutine#2, 온라인 슬롯3 @coroutine#2]
Default thread names: [온라인 슬롯3 @coroutine#3, 온라인 슬롯2 @coroutine#3, 온라인 슬롯1 @coroutine#3]
@coroutine#2는온라인 슬롯1,온라인 슬롯2,온라인 슬롯3 모두에서 실행되었다.@coroutine#3도 마찬가지다. delay()를 통해 다른 코루틴이 실행된 후 다시 돌아온 코루틴은 이전과 다른 새로운 스레드에서 실행된 것이다.
2) 다른스레드에서 실행되는 launch가 두 개라면 실행 순서를 보장할 수 없다.
앞선 예제를 다음과 같이 조금 바꿔보자.
이 경우는 먼저 선언된 launch가 항상 먼저 실행된다는 보장이 없다. 몇 번 반복하여 실행하다 보면 다음과 같이온라인 슬롯s.IO가 먼저 실행되는 경우와온라인 슬롯s.Default가 먼저 실행되는 경우가 모두 나타난다.
././/.././
/./../././
launch의 인자에 디스패처를 지정하지 않고 메인 스레드를 사용하면 ./././././로 반복된다. 먼저 선언된 launch가 먼저 실행된다. 다른 스레드에서 실행되는 경우가 아니기 때문이다.
혹여나newSingleThreadContext("MyThread")라는 것을 두 launch의 인자로 선언하면 같은 스레드라고 생각할 수 있다. 하지만 String()처럼 해당 객체가 생성될 때는 서로 다른 객체이다. 이름이 같다고 같은 스레드가 아니다. 스레드는 각각의 고유 id를 가진다.
같은 스레드를 넘겨주려면 아래와 같이 한 번만 생성하고 이것을 공유해야 한다.
val myThread = newSingleThreadContext("MyThread")
그리고 항상 새로운 코루틴을 위해서 새로운 스레드를 생성하는 것은 리소스가 많이 든다. 따라서 사용하지 않을 경우 close() 함수로 해제를 해야 한다.
3) 온라인 슬롯s.Default를 사용했을 때 cancel()이 호출되기 전에 launch 블록이 실행될 가능성이 높다.
이제 디스패처가 다른 스레드나 스레드풀에서 코루틴이 실행되도록 할 수 있다는 것을 안다. 그러면 아래와 같은 예제에서 launch 블록은 메인 스레드가 아닌 다른 스레드를 이용한다는 것도 쉽게 알 수 있다.
실행 결과는 다음과 같다.
Hello, main @coroutine#1
World! 온라인 슬롯1 @coroutine#2
runBlocking에서 사용하는 메인 스레드가 아닌 별개의 스레드를 사용하니 메인 스레드의 job.cancel()이 호출되기 전에 launch 코루틴이 실행될 수 있다. 그렇기 때문에 취소가 되지 않는 것이다. launch 블록에 delay(1)만 해주어도 취소가 된다.
반대로 별도의 디스패처를 사용하지 않고 메인 스레드에서 launch를 실행할 때 cancel() 전에 delay(1)만 주어도 cancel()이 동작하지 않는다. 찰나의 순간에 따른 차이다.
왜 디스패처가 필요해?
이제 코루틴 디스패처가 코루틴이 실행될 스레드 또는 스레드풀을 결정하는 것까진 알겠다.그래서 코루틴 디스패처를 사용하면 어떤 장점이 있을까?
코루틴 디스패처는 코루틴의 장점을 극대화하는 중요한 요소이다.
만약 UI 업데이트와 같은 작업을 수행해야 할 메인 스레드에서 코루틴을 사용하여IO 작업을 한다면 어떨까? 비효율적이고 성능 문제가 발생할 수 있다. 적절한 스레드에서 동작할 수 있도록 하는 것은 성능 최적화에 영향을 주는 것이다.
다른 상황을 들어보자. 컴퓨터 공학을 전공했거나 운영체제 관련 지식이 있다면 컨텍스트 스위칭이란 용어를 들어봤을 것이다. 컨텍스트 스위칭을 간단히 말하면프로세스나 스레드를 다른 프로세스나 스레드로 CPU의 제어권을 넘기는 것이다. 이때 기존 프로세스나 스레드의 정보를 저장하고 복원하는 과정에서 리소스가 소모된다. 따라서 스레드 전환이 자주 일어나면 리소스가 낭비되는 것이다.
코루틴을 경량 스레드라고도 표현한다. 프로세스 내부에서 스레드가 동작하듯이 스레드 내부에서 코루틴이 동작한다. 따라서 코루틴을사용하면 스레드의 전환을 줄일 수 있다. 특히적절한 디스패처를 사용하면 사용자가 좀 더 최적화할 수 있는 여지가 있다.온라인 슬롯s.IO와온라인 슬롯s.Default의 큰 차이점이 무엇이었는가? 사용하는 스레드의 수 차이였다.온라인 슬롯s.Default는CPU 집약적인 작업에 최적화되어 있다고 했는데 좀 더 풀어서 말하면외부 자원에 접근하는 빈도가 낮고 CPU의 연산 능력을 많이 사용한다는 의미다. 그것이 컨텍스트 스위칭이 무조건 적다는 의미는 아니지만온라인 슬롯s.IO와 비교했을 때 스레드를 적게 사용한다는 것을 이미 확인했다. 그렇다는 것은 컨텍스트 스위칭이 적게 발생한다는 것이다.
온라인 슬롯s.IO 조차도 최대 스레드의 수를 64개로 제한하고 있다. (사용 가능한 프로세서가 64개 보다많다면 그 값을 따른다. 병렬 처리를 하기 위한 제한이기 때문에 프로세서가 여유롭다면 64개로 제한할 필요가 없기 때문이다.) 이것 역시 과도한 컨텍스트 스위칭을 막기 위한 것이다.
요약하자.
- CoroutineContext는 코루틴이 실행되는 환경에 대한 Element들의 집합이다.
- 코루틴 디스패처는 CoroutineContext의 Element 중 하나이다.
- 코루틴 디스패처는 코루틴이 실행될 스레드 또는 스레드풀을 결정한다.
- 코루틴은 특정한 스레드에 종속되지 않는다.
- 코루틴 디스패처를 통해 성능을 최적화할 수 있다.