Kotlin 코루틴 대 자바 가상 스레드 [번역]

공부는 혼자하는 거·2023년 5월 29일
0

JAVA & Kotlin Tip

목록 보기
10/11

원문

Kotlin Coroutines vs Java Virtual Threads — A good story, but just that…

https://itnext.io/kotlin-coroutines-vs-java-virtual-threads-a-good-story-but-just-that-91038c7d21eb

구글 번역 돌리다가, 중간중간 어색한 구문은 내 맘대로 해석..중간에 불필요한 말은 짤랐다..

서문

이 아티클에서, 우리는 JVM 내에서 Continuations라고 알려진 Coroutines의 두 가지 다른 구현을 시행착오를 통해서, 살펴볼 것입니다. 이는 Project Loom의 일부분인 가상스레드와 JVM에서 DSL로 제공되는 Kotlin 코루틴입니다.

약간의 서사

최근 몇년동안의 JVM을 들여다보면, JVM 내에서 새로운 플레이어가 나타났다는 것을 알아챘을 것입니다. kotlin이 입성했습니다. 긴 이야기를 짧게 요약하자면, 코틀린은 JetBrains R&D 부서로부터 시작해서, 상트페테르부르크 인근의 Kotlin 섬의 이름을 따서 명명되었습니다.

지금까지 코틀린 이야기는 짧습니다. 그러나 현재 우리가 얼마나 멀리 왔는지 이해하기 위해서는, 기억의 길을 따라, JAVA가 어떻게 발전했는지, JAVA에서 파생한 다른 언어가 어떻게 번성했는지 이해해야 합니다. 이런 식으로 우리는 더 나은 그림을 가지고 더 나은 정보에 입각한 결론을 도출할 수 있습니다.

Java/Kotlin/Scala의 짧은 연대기

이야기를 계속 이어가기 전에, 모든 JVM 혁명을 시작한(확장된 문서에 따라) 책임있는 사람들을 존중해야합니다. 제임스 고슬링 (James Gosling)은 많은 사람들에게 자바와 JVM의 발명가로 여겨집니다. 그가 없었다면 JVM 위에 나중에 발명 된 것은 아무것도 불가능할 것입니다. 같은 방식으로 Martin Odersky는 Scala의 발명가입니다. 마지막으로 Kotlin의 경우 추가 개발을 담당하는 JetBrains 팀의 팀 리더는 Dmitry Jemerov라고 말할 수 있습니다.

위의 표는 Java Virtual Machine이라는 공통 에코시스템을 공유하는 세 가지 언어의 역사에서 주요 하이라이트를 통합하는 짧은 스케치입니다. Java는 1995년 이전부터, Scala는 2001년부터, Kotlin은 2010년부터 존재합니다. Java는 가장 오래된 JVM 언어이고 최신 언어는 Kotlin입니다. 자바는 Kotlin이 처음 시작되기 최소 15년 전에 시작되었고 Scala는 Kotlin보다 9년 먼저 시작되었습니다.

나는 정확한 위치를 찾을 수는 없었지만 Loom 프로젝트의 커밋을 살펴보면 첫 번째 커밋이 2007년에 발생했음을 알 수 있었습니다. 이것이 우리에게 말해주는 것은 Loom의 아이디어가 올해쯤에 시작되었을 가능성이 매우 높다는 것입니다. Loom은 Kotlin의 코루틴과 매우 흡사한 프로젝트로, 시스템 스레드를 별도의 독립 프로세스로 조각화하여 최대한 활용하는 데 중점을 둡니다. Loom은 이러한 프로세스를 가상 스레드라고 부릅니다. Kotlin에서는 2018년에 코루틴으로 이와 동일한 아이디어를 지원하는 실험적 릴리스가 출시되었습니다. Java의 Project Loom은 2022년에 출시될 예정입니다.

Kotlin과 관련하여 새로운 언어를 만들게 된 동기가 정확히 무엇인지 정확히 파악하기가 어렵습니다. 내가 찾을 수있는 최선은 "새로운 기능을 추가해야한다는 것" 다는 기사입니다. 이 기사에서는 Kotlin 코루틴 및 자바 가상 스레드에 대해 발견한 내용을 공유한 다음 제가 내린 훌륭한 결론을 공개하고자 합니다.

저는 Java Loom 팀의 일원이 된 적도 없고 Kotlin 코루틴 팀의 일원도 아닙니다. 이 글은 소스코드 정보, 국제회의 영상, 논문을 바탕으로 작성했습니다.

계속하기 전에 코루틴은 오래 전에 발명되었지만, 당신이 그것을 알지 못한다면 여기에 큰 계시가 있습니다. 코루틴은 매우 오래되었고 실제로 1958년보다 더 오래되었습니다. 이 용어는 Donald Knuth와 Melvin Conway에 의해 만들어진 해입니다. 여기에서 사람들은 예를 들어 codecop에 의한 자체 구현을 만들었습니다.

동기

Software 엔지니어링은 수년에 걸쳐 변화해 왔으며 의심할 여지 없이 모든 사람이 모든 것을 개선하기 위해 노력합니다. 우리는 소프트웨어를 만들고 코드를 작동시키는 것이 더 쉬워지기를 원합니다. 이를 위해 우리는 점점 더 단순하게 개발할 수 있는 구문과 의미를 만들었습니다. Kotlin이 등장했을 때 저는 Kotlin이 여러 면에서 Java보다 우수하다는 생각에 거의 즉시 매진되었습니다. 이것이 Kotlin 커뮤니티가 주로 홍보하는 것입니다. 몇 달 후 나는 그것에 대한 나의 흥분의 이유를 무너뜨리는 몇 가지를 깨달았습니다. 시간이 지날수록 Kotlin은 그저 또 다른 언어일 뿐이라는 생각이 점점 더 많이 생겼고, Kotlin을 흥미롭게 만드는 것은 Kotlin이 다르다는 것입니다. 새로운 무언가가 일상을 깨고 창의성을 위한 공간을 만듭니다. 내가 마음을 바꾸지 않은 한 가지는 올바른 방법으로 Kotlin이 Java보다 훨씬 더 아름다운 코드를 생성할 수 있다는 것입니다. 그러나 아름다움은 이 기사에서 논의하고 싶지 않은 것입니다. 이 기사의 진정한 의미는 성능입니다. Kotlin과 Java에 대해서만 논의하지는 않을 것입니다. 시스템 스레드를 사용하는 두 가지 구현과 코루틴이라는 매우 오래된 개념에 대해 논의할 것입니다. Java에서는 Loom 프로젝트에서 가상 스레드라고 하고 Kotlin에서는 이를 ... 잘... 코루틴. 양쪽의 코드를 진행하면서 피트 스톱을 만들고 코드를 서로 비교하고 차이점을 확인합니다. 그러나 먼저, 우리가 말하는 것을 정확히 이해하기 위해 약간의 이론으로 들어가고, 이것이 왜 이전에는 혁명이 아니었는지, 그리고 시스템 스레드를 보다 효율적으로 사용할 수 있도록 인터페이스와 의미론을 개발하기 위해 언어를 얻는 데 왜 그렇게 많은 시간이 걸렸는지에 대해 논의해 보겠습니다.

코루틴, 무엇인가요?

우리가 순전히 의미론적 수준에서 코루틴을 본다면 문자 그대로의 의미를 취하여 Co와 Routines를 얻습니다. 루틴은 실행되는 몇 가지 명령일 뿐입니다. 코루틴은 함께 실행되는 것입니다. 이 경우 실행은 말 그대로 원래 루틴을 일시 중단하고 완전히 다른 루틴을 시작한 다음 원래 루틴을 다시 시작할 수 있도록 하는 것을 의미합니다.

이것을 설명하기 위해 1985년으로 돌아가 인터넷의 도움으로 C++로 Epoxy로 테이블을 만드는 방법에 대한 몇 가지 지침을 보여주는 작은 프로그램을 만들었습니다(실제 Epoxy 테이블을 만들고 싶다면 이 지침을 따르지 마십시오. 에폭시로 테이블을 만들려면 안전 장비와 보호 장치가 필요하므로 먼저 정보를 얻으십시오). 왜 C++인가? 글쎄, 왜 안돼? 나아가 중립적인 관점에서 시작하는 것이 매우 중요하다고 생각합니다. 이러한 기본 사항을 올바르게 이해하면 순조롭게 진행될 수 있습니다! 이것이 메인 프로그램입니다.

(메인 에폭시 테이블 프로그램)

그래서, 우리는 많은 사례(정확히 10개)를 가지고 있으며 현재로서는 이 코드 조각이 많은걸 나타내주지는 않는 것 같습니다. 우리는 지금 당신의 관심을 끌 무언가가 있는데 그것은 pthread_self()입니다. 또 다른 것은 for-loop의 유효성 검사에 포함된 processes(1,11)입니다. 이 함수에 대해서 더 파고들어보겠습니다.

(프로세스)

오, 여기, 우리는 이상한 점을 발견합니다. 케이스 조건 1 내에서 상태에 0을 할당합니다. 이로 인해 반환되기 전에 주 스레드가 분할됩니다. 기술적으로는 2로 분할되지 않지만 런타임에 일시 중단되어 다른 하나가 시작할 수 있습니다. 즉, 루틴이 적중하면 자체적으로 일시 중단되고 기본 for 루프에 있는 모든 것을 먼저 실행한 다음에야 사례 1에 있는 항목의 실행을 완료합니다. C++에서 이것을 보면 매우 직관적이지 않은 것처럼 보일 수 있지만 코드를 실행하면 이 현상이 발생하는 것을 볼 수 있으며 메인 스레드가 다른 루틴을 일시 중단하고 다시 시작했지만 모두 동일한 스레드에 매달려 있음을 알 수 있습니다.
switch-case return ithread

(심층 코드 분석)

이것이 본질적으로 코루틴입니다. 이 C++ 예제에서는 모든 것이 비동기적으로 실행됩니다. 코루틴을 구현하는 방법은 여러 가지가 있습니다. 2000년대 후반 Loom과 Kotlin 코루틴이 금광으로 여겼던 프로젝트는 이를 탐구하고 일종의 비동기 방식으로 코루틴을 구현하는 것이었습니다. 두 언어 모두 발전했으며 둘 다 여전히 각각의 구현에서 실행되는 실험적 기능을 가지고 있습니다. 그러나 Java는 10년 후반에 훨씬 일찍 개발을 시작했지만 아직 EAB(얼리 액세스 빌드) 단계에 머물러 있습니다.

JVM(Java Virtual Threads)

Java 가상 스레드에 대해 논의하려면 Fibers, Continuations 및 Virtual Thread와 같은 몇 가지 기본 개념에 익숙해져야 합니다.

파이버: 명확하게 말하자면, 파이버는 가상 스레드를 참조하는 또 다른 방법일 뿐입니다. 여기에 대해 마법은 없습니다

가상 스레드 : 실제 행동을 더 잘 나타내기 위해 이러한 방식으로 명명되었습니다. 개발자의 경우 스레드 (플랫폼 또는 시스템 스레드)와 가상 스레드 (더 많은 프로세스를 실행할 수 있도록 독립적으로 실행되는 캐리어 스레드에 의해 실행되는 것) 사이에는 명백한 차이가 없습니다

캐리어 스레드 (Carrier Thread) : 언뜻 보면 유행에 민감한 사람들이 사용하는 용어이며 플랫폼 스레드 또는 시스템 스레드를 지칭하는 또 다른 방법처럼 보입니다. 하지만 캐리어 스레드는 그보다 훨씬 더 중요한 역할을 합니다. 캐리어 스레드는 하나의 가상 스레드가 실행되는 곳입니다. 이는 아래에서 자세히 설명할 코드를 살펴보면 더 잘 알 수 있습니다.

Continuation: 파이버와 가상 스레드는 . 연속은 결과를 산출한 후 계속 진행할 수 있도록 하는 것입니다. 이것은 모든 가상 스레드의 가장 낮은 수준이며 작동 방식입니다. 앞서 코루틴이 어떻게 작동하는지 살펴봤습니다. 이것이 바로 연속이 작동하는 방식입니다. 사실 코루틴은 연속성의 다른 이름일 뿐입니다. 이 글의 시작 부분에 있는 예제 코드에는 두 개의 가상 스레드가 있습니다. 하나는 실행을 시작할 때, 다른 하나는 텍스트로 시작할 때입니다: "종료 단계".continuations

Java 가상 스레드란 무엇입니까?

이 시점에서, 그리고 위의 내용을 통해 이 모든 연속성과 코루틴이 무엇에 관한 것인지 매우 명확하게 이해하신 것 같습니다. 같은 거죠? 이론은 같은 것 같지만 구현은 다릅니다. 이 단계에서 가상 스레드 구현의 주요 특징 몇 가지를 살펴보겠습니다(적어도 제가 보기에는):

(Loom JDK19에서 가상 스레드 시작하기)

에이 시점에서는 아무 일도 일어나지 않습니다. 우리는 plain runnable을 받고 메서드에 들어갑니다. 우리는 이미 내부에서 실행 중이며 이 코드는 코드일 뿐입니다. 일단 거기에서, Loom은 우리의 작업을 파라미터로 하는 VirtualThread를 생성하고 시작합니다. 이런 식으로 가상 스레드를 시작할 때 처음 두 매개 변수를 null로 만들고 세 번째 매개 변수를 0으로 만들고 네 번째 매개 변수를 작업합니다. 먼저 VirtualThread에 대해 자세히 알아보고 continuation이 무엇인지에 대해 보고 배운 것과 원격으로 유사한 징후가 있는지 살펴보겠습니다.

(VirtualThread Constructor for the static call in Loom JDK 19)

이 경우에서는 스케줄러가 없고, 이름도 없고, 특성도 0인 가상 스레드를 생성한다는 것입니다. 물론 이 모든 것이 무엇을 의미할까요? 여기서 몇 단계를 건너뛸 수도 있지만, 스레드 초기화에서는 ID만 할당하고 특성은 할당하지 않습니다. 이름을 부여하지 않으므로 스레드를 이름으로 식별할 수 없습니다. 적어도 기본적으로는 없습니다. 스레드를 시작하기 전에 스케줄러를 가져옵니다. 이 부분에서는 시스템 스레드에서 적절한 스케줄러를 가져오거나 가상 스레드에서 적절한 스레드를 가져오도록 하는 몇 가지 코드를 다루고 있습니다. 두 가지 유형의 스케줄러가 있는 것 같습니다. 하나는 가상 스레드용이고 다른 하나는 시스템 스레드용입니다. 이들은 실제로 재사용되는 것으로 보입니다. 새 스케줄러는 생성자에 지정된 스케줄러가 없는 경우에만 할당되며, 현재 스레드인 부모 스레드를 기준으로 할당됩니다. 스케줄러를 확보하면 마침내 현재 VirtualThread로 연속(VThreadContinuation)을 생성할 수 있고, 우리가 지정한 실행 가능한 작업을 전달할 수 있습니다. 마지막으로, 나중에 실행할 수 있도록 람다를 사용하여 프로퍼티를 할당합니다. runContinuationrunContinuation

이제 플랫폼 스레드의 스케줄러, 이름 없음, 하나의 ID, 0개의 특성으로 가상 스레드를 생성하고 여기에 연속을 할당하고 람다로 프로퍼티를 할당했습니다. 방금 생성한 스케줄러는 기본적으로 머신에서 제공한 수와 동일한 수준과 .runContinuationrunContinuationForkJoinPoolparallelisationCPU의 최대 워커 풀 256으로 생성됩니다.

여기서부터는 상당히 많은 네이티브 코드 호출이 포함되기 때문에 어떤 일이 발생하는지 설명하기가 상당히 복잡해지지만, 제가 잘 알지 못하며 이 글과는 관련이 없습니다. 하지만 이 글과 관련이 있는 것은 가상 스레드가 수명 주기 동안 거치는 상태입니다. 가상 스레드는 잠재적으로 다음과 같은 상태를 거칠 수 있습니다(모두 int 값입니다):

New 0: 스레드 시작 상태입니다.

Started 1: 가상 스레드가 시작되었습니다.

Runnable 2: 스레드가 마운트 해제된 상태이며 이 상태는 스레드에 Yielding 상태가 된 후에 할당할 수 있습니다. 현재 스레드가 실행되고 있지 않습니다.

Running 3: 스레드가 실행 중이며 마운트되었습니다.

Parking 4: 스레드에 권한이 없는 경우 스케줄링을 위해 스레드를 비활성화하기 시작합니다.

Parked 5: 정지 상태 후 양보 후 스레드가 정지됨 상태가 됩니다. 정지됨은 다시 말해, 스케줄링되기를 기다리는 중이라는 뜻입니다.

Pinned 6: 동기화된 프로세스에 의해 지연되거나 일부 IO 작업의 경우처럼 가상 스레드에서 지원되지 않는 작업을 수행할 때 스레드가 고정됩니다. 다른 IO 작업은 비블록킹 방식으로 수행됩니다. 더 정확히 말하면, 고정은 가상 스레드가 아직 사용할 수 없는 객체를 기다리는 경우 마운트 해제를 허용하지 않는 방법입니다.

Yielding 7: 스레드는 프로세서에 대한 제어권을 양보하기 위해 마운트를 해제했다가 다시 마운트가 허용되면 다시 마운트됩니다. 즉, 캐리어 스레드를 반환하는 것입니다. 이것도 컨텍스트 전환의 한 형태입니다. (0)으로 잠자면 이 상태가 즉시 트리거됩니다.

Terminated 99: 가상 스레드의 최종 상태입니다. 다시는 사용되지 않습니다.

Suspended 256: 마운트 해제 후 가상 스레드를 일시 중단할 수 있습니다.

Runnable Suspended: 스레드가 실행 가능 및 일시 중단될 수 있습니다.

Parked Suspended: 스레드를 정지 및 일시 중단할 수 있습니다.

가상 스레드가 잠자기 상태가 되어야 할 때, 가상 스레드는 연산을 수행합니다. 이를 위해서는 Yielding이라는 것이 필요합니다. Yielding을 수행하면 현재 가상 스레드를 현재 시스템 스레드에서 마운트 해제하고 다른 가상 스레드.delay에 제어권을 양보합니다.

차단 작업을 수행 중이고 스레드가 고정되어 있는 경우, 하나의 시스템 스레드는 차단되지만 다른 스레드는 차단되지 않습니다. 예를 들어 코어가 12개인 경우 11개는 가상 스레드를 관리하는 데 사용되지만 1개만 대기 중으로 차단됩니다. 차단 작업은 네이티브 코드에서 차단되는 일부 작업을 사용할 때 발생합니다(예: 스레드가 pinnedsynchronizedObject.wait()

(Continuation Yield in Loom JDK19)

Sleeping은 가상 스레드가 실행을 일시 중지하는 한 가지 방법입니다. 코드에서 실행되는 다른 가상 스레드와 다르게 동작합니다. 이 조합을 위해서는 다음과 같은 주차라는 또 다른 개념이 필요합니다. synchronisedVirtualThread.java

(Parking in Loom JDK19)

Parking은 대기열 또는 특정 IO 작업과 같은 일종의 예약된 프로세스를 사용할 때 발생합니다. 실행할 수없고 언급 된 테스트 케이스와 같이 네이티브 프로세스를 차단해야하는 경우 상태가 PARKING에서 PINNED로 변경됩니다. synchronised

(Pinning in Loom JDK19)

테스트 케이스 saveWordsParking 예제를 제공합니다.

(JDK19에서 PINNING을 유도하는 예)

그러나 Parked은 상당히 이상한 상태이며 재현 할 수 없었습니다. 이것은 이 변수 notifyJvmtiEvents 와 관련이 있습니다.이 변수는 네이티브 메소드를 사용하여 마운트 및 마운트 해제에 대해 분명히 수행합니다. 문헌에 따르면 Parked는 아무 작업도 수행하지 않고 해당 차례가 Unparked가 되고 Scheduler가 가져갈 때까지 기다리는 스케줄러의 스레드를 식별하는 상태입니다. 이는 JVM이 관리할 수 있는 차단 해제 작업(예: 네이티브 독립)의 경우여야 합니다.

Kotlin 코루틴

이전에 보았듯이 코루틴은 가상 스레드와 매우 유사합니다. 실제로 이론상으로는 둘 사이에 큰 차이가 없습니다. 그러나 구현방식은 다릅니다. 이전에 가상 스레드를 사용했던 것처럼 자세히 살펴보기 전에 Kotlin 세계의 몇 가지 용어에 대해 알아보겠습니다.

Suspend: 코루틴을 만드는 작업을 나타냅니다. suspended라고 하는 메서드는 코루틴 컨텍스트에서만 실행됩니다. 이 컨텍스트는 실행 중에 다른 컨텍스트로 전환될 수 있습니다.

delay: 지연은 일종의 수면과 비슷하지만 우리가 지시하는 한 실행 중인 코루틴을 일시 중지하거나 일시 중단합니다

coroutine: 가상 스레드와 마찬가지로 코루틴은 플랫폼 스레드에서 실행됩니다. 콘텐츠를 자동으로 전환할 수도 있습니다.

Kotlin 코루틴이란?

지금쯤이면 이미 알고 계시겠지만, Kotlin은 프로그래머가 애플리케이션을 쉽게 빌드할 수 있도록 하는 것을 목표로 몇 가지 새로운 구문을 지원하는 단순한 DSL에 불과합니다. 이 때문에 코드와 바이트코드를 처음 해석할 때 약간의 혼란이 있을 수 있습니다. 따라서 우리가 자주 사용하는 IDE에서 Java의 경우처럼 startVirtualThread와 같은 것을 클릭하는 대신 Kotlin의 경우 일시 중단 코드를 입력하는 방법을 찾아야 합니다. 먼저 다음과 같은 예제를 살펴봅니다:

(코루틴의 예)

IDE에서 다음을 수행하는 다양한 방법을 찾을 수 있습니다. 다행스럽게도 Intellij에는 컴파일 된 바이트 코드를 볼 수있는 도구가 있습니다.

IntelliJ의 Kotlin 도구

여기에서 Decompile 버튼을 클릭할 수 있습니다.

마침내 이런 종류의 코드를 얻습니다.

꽤 지저분하죠? 이것이 현재 2022년에 Kotlin 코드를 Java 코드로 디컴파일하는 방법입니다. 실제로 Java 코드 자체는 아니지만 실제로 JVM으로 변환되는 방법에 대한 창을 제공합니다. 이 단계를 건너뛰고 코드가 컴파일되는 방식을 정확히 보려면 명령줄로 이동해야 할 것입니다. 호기심에서 커맨드 라인으로 이동하여 대상 디렉토리의 파일을 나열하면 컴파일 된 Java 클래스에서 일반적으로 볼 수있는 것보다 훨씬 많은 파일을 볼 수 있습니다.

우리는 꽤 많은 클래스를 가지고 있고 일부는 실제 메소드 이름을 가지고 있습니다. 보기에는 그리 좋지는 않지만 Kotlin은 Kotlin이 Java 위에 있는 레이어이기 때문에 이 작업을 수행합니다. 즉, DSL(Domain Specific Language)입니다. 이것은 우리가 자바 코드에서 얻는 것처럼 바이트 코드 클래스를 얻지 않을 것임을 의미합니다. 결국, 바이트 코드는 컴파일 타임에 내부적으로 생성되기 때문에 Java 코드가 필요하지 않습니다. 또 한 가지 흥미로운 점은 기본적으로 Intellij를 사용할 때 이러한 파일을 모두 볼 수 없다는 것입니다. 당신이 볼 수 있는 유일한 것은 해석된 방식으로 Kotlin에 대응하는 것입니다.

Anyways, 디컴파일 된 코드로 돌아가 보겠습니다. 우리가 Continuation을 사용하고 있다는 것을 알고 계셨습니까? 우리는 전에 Java에서 그것을 본 적이 있습니까? Java에서와 같은 방식으로 자세히 살펴 보겠습니다.

(Continuation in Kotlin)

a는 인터페이스이고 a와 function.ContinuationCoroutineContextresumeWith가 있음을 알 수 있습니다.

코루틴을 평가할 때 전체 라이브러리가 Kotlin 소스 코드로 개발되었기 때문에 코루틴이 Java로 어떻게 변환되는지 확인하기가 상당히 어렵기 때문에 여기까지입니다. 제가 말하고자 하는 요점은 현재로서는 Kotlin 코루틴이 Java 가상 스레드와 크게 다르지 않아 보인다는 점입니다. 하지만 다른 한편으로, 소스 코드가 Kotlin으로 작성되었다고 해서 우리가 읽을 수 없다는 의미는 아닙니다. 그럼 한번 해보겠습니다.

SafeContinuation

SafeContinuation은 Continuation의 구현입니다. expect는 native와 동일한 방식으로 Kotlin에서 사용되는 키워드입니다. 즉, Kotlin에서 이는 구현이 플랫폼에 따라 다르다는 것을 의미하며 물론 액세스하기도 쉽지 않습니다. 코루틴 코드를 더 자세히 살펴보면 어떤 것도 이해하기가 상당히 어려워집니다. Java에서는 전체 JDK를 통해 디버깅 할 수 있지만 Kotlin에서는 매우 어려워지며 이것이 suspend 가 Intellij의 키워드로 해석되고 일반 코드가 아닌 키워드로 해석된다는 사실과 관련이 있다고 가정합니다. 따라서 우리는 Continuation과 같은 것을 쉽게 디버깅할 수 없습니다. 하지만 잠깐만! 물론, 우리는 할 수 있습니다!. Kotlin을 사용하면 Java와 마찬가지로 코드가 어디에 속할지 추측해야 할 때가 있습니다. 따라서 DispatchedTask.kt에서 run 메서드를 열어 엉뚱한 추측을 합니다.

DispatchedTask 실행 메서드

만약 당신이 나의 Kotlin 예제를 실행하면 코드가 'here'에 속한다는 것을 알 수 있습니다. 이 디스패치된 작업을 통해 코루틴을 실행할 수 있습니다.

Kotlin에서는 여러 가지 방법으로 코루틴을 시작할 수 있습니다. 우리는 함수에서 suspend 를 사용할 수 있고, 그것을 호출 할 무언가를 얻을 수 있으며, 우리는 와 다른 많은 방법을 사용하여 구현할 수있는 코루틴 컨텍스트 를 시작할 수 있습니다. 테스트 예제에서 우리는 다음과 같은 것을 사용하고 있습니다 :withContextrunBlocking

(Kotlin 코루틴 만들기의 예시)

Intelij는 코루틴이 시작되는 위치를 파악하는 데 도움이 될 수 있습니다. 이 예에서는 실제로 3개의 코루틴을 만듭니다.

suspend 호출자의 컨텍스트를 사용하여 코루틴을 만듭니다.

GlobalScope.launch는 글로벌 맥락에서 코루틴을 시작합니다(강력히 권장). 항상 대신 coroutineScope를 사용하는 것이 좋습니다.

withContext(IO) 는 IO 컨텍스트에서 코루틴을 만듭니다.

키워드를 사용하면 코루틴을 생성합니다. 예시에서는 보이지 않습니다. 이 함수는 부모 함수: 와 연관되어 있습니다. 이를 위해 코드에서 이 예제를 찾아보세요. 그런 다음 새로운 . 는 전역 컨텍스트를 가진 코루틴을 시작합니다. 물론 그 아래에서 .suspendsuspend를 사용하여 다른 코루틴을 시작할 수 있습니다. fun generalTest()GlobalScopeGlobalScopewithContext(IO)

(Kotlin 코루틴 라이브러리에서 코드 실행)

에서 코루틴 구현에 대해 자세히 살펴보면 코루틴에 모드와 state.Tasks.kt가 있다는 것을 알 수 있습니다.

코루틴은 다음과 같은 모드를 가질 수 있습니다:

TASK_NON_BLOCKIN 0: 태스크가 CPU에 바인딩되어 있으며 블록되지 않습니다.

TASK_PROBABLY_BLOCKING: 1: 태스크가 차단될 가능성이 있습니다. 이는 힌트처럼 작동하며 가상 스레드에서 본 것과 마찬가지로 스케줄러에 시스템 스레드가 필요할 수 있음을 알려줍니다.

CoroutineScheduler.kt에서 Kotlin 코루틴 워커에 사용할 수 있는 상태는 다음과 같습니다:

CPU_ACQUIRED: CPU 토큰을 획득하고 이를 사용하여 비차단 방식으로 작업을 실행하려고 시도합니다.

BLOCKING: 태스크가 차단 중이며 이를 허용하는 유일한 모드는 TASK_PROBABLY_BLOCKING입니다.

PARKING: 스레드를 파킹하며, 앞서 살펴본 것과 마찬가지로 스레드를 일시적으로 실행할 수 없을 때 파킹이 발생합니다.

DORMANT: 다른 작업을 실행할 수 있을 때까지 휴면 상태를 유지합니다. 파킹은 워커가 이미 작업을 담당하고 있다는 것을 의미하므로 파킹과는 다릅니다.

TERMINATED: 작업자의 마지막 상태입니다.

마지막으로, 코루틴은 DispatchedCoroutine.kt에서 이러한 상태를 갖습니다:

RESUMED 2: 코루틴이 아직 미결정 상태일 때만 설정할 수 있습니다. 코루틴이 실행을 진행 중입니다.

SUSPENDED 1: 코루틴이 아직 결정되지 않은 경우에만 설정할 수 있습니다. 코루틴이 일시 중단되었습니다.

UNDECIDED 0: 코루틴의 초기 상태(소스 코드에서는 _decision으로도 설명됨)

코루틴을 실행할 때 익숙한 상태입니다. 디자인 시간 동안에는 워커가 어떻게 작동하는지에 대해 크게 신경 쓰지 않으며, 모드에 대해서도 신경 쓰지 않습니다. 하지만 코루틴에 대한 이러한 기본 개념을 알고 있거나 최소한 코루틴이 존재한다는 사실을 알고 있으면 매우 유용할 수 있습니다.

요약하자면, 코루틴은 일시 중단 함수, , 또는 실행으로 시작할 수 있으며 코루틴 컨텍스트 외부에서는 작동하지 않습니다. 이러한 컨텍스트를 만들어야 하는 경우, withContext withContext launch runBlocking suspend 같은 것을 사용해야 합니다.

가상 스레드와 코루틴의 유사점

이제 코드를 검토했으므로 이론을 자세히 살펴보면 코드를 더 잘 이해해 보겠습니다. 코루틴과 자바 가상 스레드에 대한 이론은 인터넷의 어느 곳에서나 찾을 수 있으며 테스트를 수행 한 리포지토리에는 이에 대한 정보에 대한 많은 링크가 포함되어 있습니다. 아마도 이 시점에서 두 가지 구현에 대해 알아야 할 것은 다음과 같습니다.

  1. 둘 다 1958년에 발명된 최초의 코루틴 원리를 기반으로 합니다. 이것은 참으로 새로운 개념이 아닙니다

  2. 둘 다 한 함수 런타임을 일시 중단하여 다른 함수 런타임으로 넘어갈 수 있다는 아이디어를 기반으로 합니다.

  3. 둘 다 pinning, dormant 및 parking과 같은 개념을 사용하여 기본 스레드에 대한 아이디어를 구현합니다.suspendwaiting

  4. 둘 다 시스템이 아닌 JVM에 의해 관리됩니다

  5. 둘 다 완전히 새로운 플랫폼 스레드를 만들지 않고 이미 실행 중인 스레드를 활용합니다. 스레드 풀에서 시작되었습니다. 자바 가상 스레드의 경우 ForkJoinPool, Kotlin 코루틴의 경우 CoroutineScheduler를 사용합니다.

  6. CPU 코어만큼만 플랫폼 스레드를 가질 수 있지만 보유한 코어 수까지 병렬화 수준으로 다양한 프로세스를 시작하고 기계가 처리할 수 있는 한계까지 원하는 만큼 동시에 시작할 수 있습니다. 우리가 병렬로 더 많이 수행한다는 착각은 가능할 때마다 시스템 스레드가 차단되지 않도록 함으로써 만들어집니다.

  7. 둘 다 기술적으로 sleep 하지 않습니다. 적어도 그들은 상태에서 sleep 안 합니다. Java에서는 이 작업이 원활하게 수행되며 스레드에 PARKING 상태를 부여하고 허가를 부여하여 비차단 기술을 사용합니다. Parking은 즉, 잠자는 것을 의미하고 Parking을 해제하는 것은 깨어나는 것을 의미합니다. Kotlin에서 지연은 현재 실행이 나중에 실행되도록 예약되도록 합니다. 그러나 심층 분석을 통해 Parking 및 Unparking도 구현의 일부임을 알 수 있습니다.blockingThread.sleep

  8. 둘 다 PINNING을 수행하는 방법이 다릅니다. Java에서 고정(PINNING)은 스레드를 캐리어 스레드에 단단히 고정하기 위해 수행됩니다. 이것은 동기화 된 메서드에서 발생합니다. Kotlin 코루틴에서 실행은 단일 CPU 스레드로 PINNED됩니다. 일시 중단 및 다시 시작 작업은 코루틴이 끝날 때까지 동일한 스레드에서 실행되도록 합니다. 같은 방식으로 Kotlin은 동기화 된 메소드를 가지고 있으며 물론 PINNING도 사용합니다

  9. 두 경우 모두 Thread는 네이티브 스레드를 둘러싼 얇은 래퍼입니다.

Java 가상 스레드 테스트 구현

이러한 테스트 세트를 수행하기 위해 시간과 공간의 복잡성이 다른 다양한 방법의 실행 시간을 측정할 수 있는 프레임워크를 만들었습니다. 여러 종류의 진행 상황에 충분한 변화를 주고 여러 가상 스레드를 동시에 배포할 때 이 모든 것이 어떻게 진행되는지 확인하는 것이 목적입니다. 이 테스트에서는 특정 가상 스레드 하나가 실행되는 데 걸리는 개별 시간을 측정하는 데는 관심이 없습니다. 대신 전체를 측정하여 모든 것이 어떻게 진행되는지 보고 싶습니다. 성능 측정을 위한 코드에는 보고서 코드, 파일 관리 코드, CSV 파일 생성 알고리즘도 포함되어 있어 한 시점에 배포할 수 있는 Java 가상 스레드 수를 결정하는 데 도움이 됩니다. 각 개별 테스트의 실행, 수행 및 지속 시간을 측정하기 위해 다른 인수를 포함하여 람다를 매개변수로 받는 메서드를 살펴보겠습니다:

(Java에서 개별 테스트 수행)

제가 여기서 만든 것은 Kotlin에서 배운 몇 가지 사항에서 영감을 얻은 방법일 뿐입니다. 개별적으로 살펴보겠습니다.

  1. testName은 메서드의 이름입니다.
  2. methodName은 어떤 메서드를 테스트하고 있는지 알려주는 매개 변수입니다. Kotlin에서는 리플렉션을 통해 큰 번거로움 없이 메서드 이름을 쉽게 얻을 수 있다는 것을 나중에 살펴보겠습니다. 하지만 Java에서는 여전히 메서드 이름을 하드코딩하고 이를 입력 매개변수로 사용해야 했습니다.
  3. 시간 복잡도는 말 그대로 원하는 것을 넣을 수 있는 문자열이지만 테스트 중인 메서드의 큰 O 표기법을 표현하는 데 사용하도록 되어 있습니다. 이는 메서드 복잡성이 성능에 어떤 역할을 하는지 확인하기 위해 중요합니다.
  4. spaceComplexity도 말 그대로 문자열이지만 이 경우에는 공간 복잡성에 사용됩니다.
  5. sampleTest는 로그에서 단일 테스트의 출력 조각을 볼 수 있도록 공급자에 불과합니다.
  6. toTest는 실행할 실제 테스트입니다.
  7. repeats은 실행할 횟수입니다.

명확히 하기 위해, 그리고는 작은 입력에서 서서히 증가하는 입력으로 점진적으로 테스트해야 합니다. 진행 상황은 추후 제 웹사이트(http://joaofilipesabinoesperancinha.nl)에서 확인할 수 있습니다. 진행 테스트는 개인용 컴퓨터의 한계로 인해 실행하기가 다소 어려우므로 이 두 가지 요소는 이 글의 결과에 중요한 역할을 하지 않습니다. 각 메서드의 개별 구현은 이 글을 위해 만든 프로젝트에서 쉽게 읽을 수 있습니다.timeComplexityspaceComplexity

startProcessAsync는 startVirtualThread 메서드가 호출되는 곳입니다:

(startProcessAsync를 사용하여 가상 스레드를 테스트합니다.)

코루틴으로 낙하산

코루틴은 Java Virtual Threads보다 약간 더 복잡한 패러다임을 가지고 있습니다. 시작 방법에 대한 다양한 옵션을 제공하기 때문입니다. 자바 가상 스레드에도 이 기능이 있지만 Kotlin은 이러한 변경 사항을 수용하기 위해 자체 구문을 변경하여 몇 단계 더 나아갑니다. 그러나 그 복잡성은 매우 복잡하게 만듭니다. 나에게는 매우 흥미롭지만 일반 개발자에게는 너무 먼 단계 일 수 있습니다. 짧은 문장에서 Kotlin 코루틴을 사용하면 비동기적으로 실행을 시작하고 반환 객체를 기다리지 않고 반환 객체를 기다리지 않고 현재 코루틴을 일시 중단하고 대신 다른 코루틴을 실행할 수 있습니다. 컨텍스트를 실행하기 위한 4가지 다른 추상화가 있으며 delay라는 이름으로 "절전 모드"를 사용할 수 있습니다 , 이는 결국 휴면 작업을 예약하는 것이며, 활성화된 코루틴 기능을 사용하여 특수 IO 관련 컨텍스트를 만들 수 있습니다. 이것이 이 섹션에서 살펴볼 내용의 기본 사항입니다. 다음을 살펴 보겠습니다.

(코루틴을 실행하는 다양한 방법)

많은 튜토리얼에서 사람들이 스레드 모양의 물결선을 사용하여 코루틴이 작동하는 방식을 나타내는 것을 볼 수 있습니다. 나는 전에 그렇게 하곤 했지만 내 자신의 의견으로는 약간 오해의 소지가 있을 수 있습니다. 또는 그것이 동수들을 위한 소개 표현일 뿐이라고 주장할 수 있습니다. 그러나 코루틴은 Threads처럼 작동하지 않지만 코드의 일부 지점에서 이러한 인상을 받을 수 있습니다. 이 시점에서 위의 모든 내용을 읽으면 내가 왜 이런 말을 하는지 이미 이해했을 것입니다.

CoroutinesShortExplained.kt 클래스에 있는 위의 코드를 실행하면 이 코드의 대부분이 main 스레드에서 실행되는 것을 볼 수 있습니다. 그래서 당신은 스스로에게 물어볼 수 있습니다, 왜 우리는 단일 스레드에서 2 초를 기다린 다음 2 초를 기다릴 수 있고, 모든 것을 실행하는 데 정확히 2 초가 걸릴까요? 이는 (코루틴의 경우)와 달리 작업이 현재 코루틴을 나중에 실행하도록 예약하고 파킹하기 때문입니다. 이렇게 하면 주 스레드가 해제되어 실행을 계속할 수 있습니다. 2초가 지나면 코루틴이 주차 해제되고 다시 시작됩니다. async를 사용하면 launch와 동일한 작업을 수행하지만 이 경우 수신자가 반환하는 모든 것을 반환합니다. 이 경우 는 아무 것도 반환하지 않으므로 Unit일 뿐입니다. 마지막으로 이 기능의 전체 대기 시간에 500ms 를 추가하는 효과가 발생합니다. 그 이유는 컨텍스트 전환을 수행하기 때문입니다. 호출 코루틴을 일시 중단하고 실행을 실행하여 마지막에 호출자에게 다시 반환합니다. 이것은 실행하는 시스템 스레드에 관계없이 발생합니다. 이것이 전체 코드를 실행할 때 런타임에 약 3500ms 를 얻는 이유입니다.Thread.sleepdelaywithContextwithContext

(짧은 코루틴 예제를 실행한 결과)

이것들은 기본이지만, 다른 맥락이 무엇을하는지에 대한 아이디어를 갖는 것도 중요합니다.

IO: 이 컨텍스트는 PINNING 중에 Java Virtual Threads가 수행하는 것과 동일한 방식으로 차단 작업 중에 코루틴을 관리합니다. 실행 결과 번호 2에서 이를 확인할 수 있습니다. 가능한 경우 IO 작업을 비차단 방식으로 실행할 수 있도록 IO 작업 중에 사용하도록 의도적으로 만들어졌습니다.

Default: 작동하기 위해 최소 2개의 코어를 사용하며 기본적으로 사용 가능한 코어만큼 많은 스레드를 포함하는 스레드 풀을 사용합니다. 실행 결과 번호 7에서 이를 확인할 수 있습니다. 가능한 경우 사용 가능한 JVM 스레드 풀과 다른 스레드를 사용합니다. 그렇지 않으면 첫 번째 것을 사용합니다.

Unconfined: 디스패처가 반드시 동일한 스레드에서 계속 실행되지는 않음을 의미합니다. 실행 결과 번호 6에서 이를 확인할 수 있습니다. 그 기준은 사용 가능한 첫 번째 스레드를 사용하여 매우 빠르게 만드는 것입니다. 이 스레드와 의 미묘한 차이점은 가능한 경우 첫 번째 다른 스레드를 선택하는 반면 디스패처는 사용 가능한 첫 번째 스레드를 선택할 수 있다는 것입니다.DefaultDefaultUnconfined

Main: 이것은 플랫폼에 따라 다르며 존재할 필요는 없습니다. 때때로 Android 관련 컨텍스트라고도 불리지만 실제로는 실행중인 플랫폼에서 정의하는 대로 구현하는 것을 의미합니다.

Project Loom 에서는 더 이상 차단 작업으로 간주될 수 없습니다. 적어도 엄격하게는 아닙니다. 그러나 Kotlin 코루틴을 실행할 때 실행 스레드는 가상 스레드로 간주되지 않습니다. 대신 Kotlin 코루틴 코어 라이브러리에서 제공하는 Worker는 인터페이스의 구현이므로 코루틴이지만, 그러나 의 유형이 아니기 때문에 절전 모드로 예약되지 않고 대신 전체 실행을 차단합니다.Thread.sleepWorkerThreadWorkerThreadVirtualThread

(Project Loom에서 가상 스레드 잠자기 — JDK 19)

코루틴 테스트 구현

코루틴 테스트 함수의 구현은 Java 메서드와 매우 유사하지만 자세히 살펴보는 것은 중요합니다.

(Kotlin에서 개별 테스트 실행)

비록 같은 것처럼 보이지만 약간의 차이가 있습니다. 데이터를 파일에 저장하고 싶고 모두 차단하지 않기를 원하기 때문에 IO 컨텍스트에서 코루틴으로 전체 프로세스를 시작합니다. 이를 달성하면 비동기 컨텍스트에서 테스트할 메서드를 시작할 수 있습니다.

(Performing on individual tests in Kotlin)

테스트하기 전에

이 글을 작성하기 어려웠던 한 가지는 여기서 목표를 명확하게 설명하는 것이었습니다. 코루틴과 관련하여 가상 스레드의 성능과 그 반대의 성능을 측정하려는 것일까요? 물론입니다! 가상 스레드와 코루틴은 성능 문제를 해결하기 위해 만들어진 것일까요? 짧은 대답은 '아니요'입니다! 긴 대답은 복잡합니다. 연속성이 해결하고자 하는 문제는 리소스 부족입니다. JVM이 동시성을 처리하도록 함으로써 이제 구조화된 동시성 방식으로 코드를 작성할 수 있고, 여러 프로세스를 동시에 트리거할 수 있으며, 이를 캡슐화할 수 있습니다.

Java Virtual Threads와 Kotlin 코루틴을 모두 구조화된 동시성 방식으로 프로그래밍할 수 있는 이유를 설명하는 것은 그 자체로 완전히 새로운 기사가 될 수 있지만 간단한 정의를 통해 상식을 사용하면 이것이 왜 그런지 즉시 알 수 있습니다.

구조적 동시성이란 동시 함수의 수명이 깔끔하게 중첩되어 있음을 의미합니다.

우리는 함수를 트리거하지만 반드시 실행을 시작하지는 않습니다. 플랫폼 스레드는 공간과 시작 시간을 차지하는 매우 비용이 많이 드는 프로세스이며 컴퓨터의 코어 수에 따라 제한됩니다. 이것이 실제로 그리고 연속성을 구현한 결과 의미하는 바는 갑자기 리소스가 너무 많아져 동시 및 비동기 프로그래밍이 이제 노력할 가치가 있는지에 대해 이미 논의가 시작되었다는 것입니다. 제가 하는 테스트는 이러한 논의의 양쪽에서 구현에 어려움을 겪는 지점까지 리소스를 소진할 수 있도록 하는 것입니다. 바로 이때 성능 테스트가 필요합니다. 리소스가 고갈되었을 때 연속을 관리하는 것은 지능적인 방식으로 수행되어야 하며, 이것이 제가 이 두 가지 구현을 강조하는 이유입니다. 코루틴이 가상 스레드보다 훨씬 낫다는 것을 발견할 수도 있고, 가상 스레드가 훨씬 더 낫다는 것을 발견할 수도 있습니다. 아니면 두 구현 간에 큰 차이가 없는 것처럼 보였기 때문에 실제로 차이가 전혀 없는 것을 발견할 수도 있습니다.

물론 이러한 테스트를 생성할 수 있도록 많은 코드가 내장되어 있습니다. 애플리케이션의 루트에서 실행하면 디렉터리가 생성되는 것을 볼 수 있습니다. 그 안에는 두 개의 디렉토리와 . 여기에 테스트 결과가 저장됩니다. 각 디렉터리에는 두 가지 유형의 파일이 생성됩니다. 읽을 수 있는 파일과 읽을 수 없는 파일이 몇 개 있습니다. 이러한 파일은 쌍으로 생성됩니다. 한 파일에는 메서드 이름이 포함되고 다른 파일에는 메서드 이름이 포함되지만 -ms로 끝납니다. 첫 번째 파일의 처음 두 열에는 /별로 시작 및 종료 타임스탬프가 포함되어 있습니다. 세 번째 열에는 해당 프로세스를 수행한 실행 중인 스레드의 이름이 포함되어 있습니다.make clean build-rundumpjavakotlinmardowncsvcsvvirtual-threadcoroutine.

마지막으로, 루트에는 가능한 한 동일한 방식으로 구현된 다양한 메서드에 대한 간단한 비교 보고서가 포함된 또 다른 파일이 와 에 생성됩니다. 이 파일은 markdownJavaKotlinLog.md라고 합니다.

하지만 두 기술의 이론을 뒷받침하는 또 다른 시각화를 살펴볼 필요가 있습니다. 아이디어는 이전 실행을 일시 중단하는 동안 다른 것을 실행할 수 있다는 것입니다. 가상 스레드는 이와 비슷하게 작동하며 이것은 지나치게 단순화된 표현일 뿐입니다:

(가상 스레드 설명)

코루틴은 실제로 동일한 종류의 구조를 제공하며 다시 단순화된 또 다른 예를 제공하겠습니다.

(코루틴 설명)

낮은 수준에서 구현되는 방식에 관계없이 두 경우 모두에서 일어나는 유일한 일은 사용 가능한 스레드 간의 전환입니다. 플랫폼 스레드만 사용하는 동시 환경에서는 블로킹 호출을 하면 항상 블로킹 호출이 완료될 때까지 기다린 후에야 계속할 수 있습니다. 코루틴 또는 컨티뉴이션은 가능한 한 차단할 항목을 피함으로써 스레드를 최대한 탐색합니다. 차단 호출을 기다리는 경우 해당 코루틴이 완료되면 해당 코루틴으로 돌아가지만, 그 동안 다른 코루틴이 다른 스레드나 심지어 같은 스레드에서 이동하도록 놔둡니다. 이제 구조화된 동시성 방식으로 구현할 수 있게 되었지만, 원하는 경우 코드에서 명시적으로 수행해야 하는 작업입니다.

낮은 수준에서는 다를 수 있지만, 제가 보기에는 높은 수준에서는 Kotlin 코루틴과 Java 가상 스레드(예전에는 파이버라고도 함)가 완전히 동일합니다.

이 글을 좀 더 흥미롭게 만들기 위해 이 모든 알고리즘이 실행될 데이터 소스를 작은 개발 소설로 만들었습니다. 시간이 길어질수록 두 가지 다른 구현이 작동하기가 더 어려워질 것입니다. 이 작은 소설은 루시라는 한 여성이 삶이 너무 힘들어졌을 때 남겨둔 과제에 직면하고 다시 활동적인 삶으로 돌아오기 위해 고군분투하는 이야기를 담고 있습니다. 이 소설은 프로젝트 저장소. GoodStory.md에 있는 파일에서 모두 확인할 수 있습니다.

이 이야기는 제 개인적인 삶에서 영감을 얻었습니다. 이 이야기는 삶의 의미를 찾는 젊은 여성 루시를 중심으로 전개되며, 여전히 세상의 무게를 어깨에 짊어지고 있지만 아직 끝나지 않았다는 것을 상기시켜주는 격렬한 심장 박동을 가지고 있습니다. 인생은 여전히 루시에게 많은 것을 제공합니다. 이 이야기는 상상의 신과 캐릭터를 통해 은유적인 방식으로 전달됩니다. 감정의 구체화와 감정이 어떻게 발현될 수 있는지를 다룹니다.

테스트 결과

앞서 언급했듯이 이러한 테스트를 실행하는 가장 좋은 방법은 명령줄을 사용하는 것이지만 IntelliJ를 통해 실행할 수도 있습니다.

Intellij를 통해 실행하면 적어도 두 개의 기본 클래스를 실행해야합니다. 하나는 자바용이고 다른 하나는 Kotlin용입니다. 이들은 각각 GoodStoryJava.java 및 GoodStoryKotlin.kt입니다. 다음 매개 변수를 사용하여 실행해야 합니다.

-f docs/good.story/GoodStory.md -lf Log.md  -dump dump

특히 Java의 경우 JDK19 기능을 활성화해야 합니다.

--enable-preview

만약 VisualVM이 있으면 동시에 실행해 주세요. VisualVM이 충돌하기 직전에 다음 스냅샷을 가져올 수 있었습니다.

(JVM(Java Virtual Threads) 캡처)

Kotlin 코루틴 프로젝트에서 동일한 방식으로 이를 캡처할 수 있었습니다.

(Kotlin 코루틴 캡처)

몇 가지 차이점이 있지만 이는 이름 차이일 뿐입니다. 두 캡처 사이에서 Java 가상 스레드에는 ForkJoinPool-1-worker-N이, Kotlin 코루틴에는 DefaultDispatcher-worker-N이 있습니다. 이러한 워커는 코루틴, 코루틴 컨텍스트, 컨텍스트 전환 및 시스템 스레드에 코루틴 할당을 조정하는 역할을 담당합니다. Java ForkJoinPool은 최대 256개의 워커를 설정하기 시작합니다. 코루틴 스케줄러는 최대 2097150개의 워커 설정으로 시작합니다.

주어진 시간에 얼마나 많은 가상 스레드 또는 코루틴이 실행되고 있는지 파악하기 위해 몇 가지 CSV 파일을 만들었습니다. 이 수치는 정확하지 않은데, 그 이유는 이 두 종류의 프로세스가 연속적으로 실행되고 각 실행마다 컨텍스트가 전환되지 않는다고 가정하고 있기 때문입니다. 그러나 이제 우리는 이것이 항상 사실이 아니라는 것을 알고 있습니다. 어쨌든 살펴볼 만한 가치가 있습니다. 이 두 프로젝트에서 실행한 가장 무거운 프로세스 중 하나를 살펴보겠습니다. 예를 들어 메서드/함수: 에서 어떤 일이 발생하는지 확인해 보겠습니다. 이 메서드는 얼마나 많은 단어가 두 번 이상 반복되는지 확인합니다. 즉, "개"라는 단어가 두 개 발견되면 한 번 반복된 것입니다. 다른 모든 "개"가 발견될 때마다 그 개수에 하나를 더 추가합니다. Java에 대한 횟수 생성을 살펴보면 주어진 시간에 활성 가상 스레드의 수가 12:repetitionCount

JVM(Java Virtual Threads)의 반복 횟수

Kotlin에서 주어진 시간에 활성 코루틴의 수가 최대 63개까지 증가한 것을 볼 수 있습니다.

Kotlin 코루틴의 반복 횟수

어떻게 이런 일이 일어날까요? Java 가상 스레드의 경우 한 번에 12개만 활성화되는 것은 당연한 일입니다. Kotlin 코루틴의 경우 이상합니다. 이 경우 무슨 일이 일어났는지는 명확하지 않지만, 코루틴이 실행 도중에 컨텍스트를 변경하거나 어떤 이유로 일시 중단되는 경우 당연히 시작 및 종료 타임스탬프가 평소보다 긴 델타를 포함하게 되고, 이 결과는 우리가 시작한 비동기 프로세스가 한 번 시작하면 일시 중단되지 않고 계속 실행된다는 초기 가정에 적용되지 않기 때문에 63이라는 숫자는 잘못된 결과라고 추측합니다. 내 컴퓨터의 코어 수가 그 정도이기 때문에 12개 또는 그 이하가 나와야 합니다. 63개가 아니라! 이 시점에서는 바랄 수 밖에 없습니다.

마지막으로 구현된 각 알고리즘에 대해 서로 다른 10000회 반복 실행을 비교할 수 있는 일반적인 결과를 살펴봅시다:

결과 프레임 한 시점

표를 보면 거의 모든 경우에서 거의 동일한 복잡도를 가진 메서드/함수에 1만 개의 가상 스레드 또는 코루틴을 던지는 데 걸리는 시간이 크게 다르지 않다는 것을 알 수 있습니다. 실제로 좀 더 자세히 확대하면 프로젝트 룸이 성능 면에서 더 나은 것 같다는 생각이 들 정도입니다. 어쨌든 결론을 내리기에는 충분하지 않습니다. 이 시점에서 저는 제 로컬 컴퓨터의 한계를 다 써버렸고 이 테스트에서 충분히 작동했습니다. 테스트 내내 프로젝트 룸의 가상 스레드가 코루틴보다 더 나은 성능을 보인다는 징후가 있었지만, 앞서 말씀드린 것처럼 확실한 결론은 아닙니다. 단지 상관관계, 즉 하나의 아이디어일 뿐입니다. 어느 쪽이 다른 쪽보다 더 낫다는 것을 확실히 증명하지는 못했습니다. 제가 증명할 수 있었던 것은 현재 현지 환경에서 동일한 문제를 해결하기 위한 이러한 접근 방식 중 어느 것도 의심할 만한 것이 전혀 없다는 것입니다. 둘 다 동일한 수준에서 좋은 것으로 보이며, Java 가상 스레드가 더 잘 수행한다는 약간의 징후는 여전히 단지 징후일 뿐입니다. 이것이 단지 표시일 뿐인 또 다른 이유는 다른 경우에도 동일한 테스트를 실행할 수 있었는데, 모든 코루틴 구현이 Java 가상 스레드보다 더 나은 성능을 보였기 때문입니다. 이는 대부분 Java 가상 스레드를 선호하는 것처럼 보이는 빈도일 뿐, 결론을 내릴 수 있는 중요한 자료는 아닙니다. 어쩌면 결론을 내릴 수 없다는 것 자체가 이미 결론일 수도 있지만, 판단은 여러분께 맡기겠습니다.

결론

Continuations에 대한 동일한 아이디어의 두 구현을 비교해 보았지만 실제로는 큰 차이점을 못했습니다. Kotlin 코루틴과 Java Virtual Threads는 모두 훌륭한 기술이라고 생각합니다. 코루틴으로 시스템을 소진하고 이를 최적화하기 위해 모든 종류의 알고리즘이 작동하도록 강제할 때 성능에 큰 차이가 없었습니다.

중요한 것은 여기에 있습니다. Kotlin은 여기에 있고 Java도 계속 유지될 것입니다. 이 글의 요점은 두 언어가 무엇을 가지고 있는지 잘 살펴보기 위해 양 당사자를 이 토론으로 이끄는 것이었습니다. Kotlin은 2010년에 발명되었으며 Java는 1995년부터 존재합니다. Scala가 만들어진 것과 마찬가지로 Kotlin도 "이전에는 사용할 수 없었던 기능을 제공하기 위해" 만들어졌습니다. 저로서는 삼키기 힘든 약이네요. 그 이유를 아시나요? Kotlin에서 사용할 수 있고 Kotlin에서 "필요"하다고 말하는 모든 기능은 Java에서도 항상 사용할 수 있기 때문입니다! 스타일만 다를 뿐입니다. 이는 오늘날 우리가 관용적 Kotlin이라고 부르는 것부터 오늘날 우리가 관용적 Java라고 부르는 것까지 다양합니다.

Java 8부터 람다를 사용하게 되었는데, 사실 2014년에 Java가 더 나은 솔루션의 부재에 대해 고민하기 시작한 것은 람다가 처음이었습니다. 람다는 Kotlin의 리시버가 하는 것과 같은 방식으로 for, while 및 {} while과 정확히 같은 작업을 수행합니다. 모든 것을 지옥처럼 느리게 만듭니다! 고가용성 애플리케이션을 위한 알고리즘을 구현하거나 해커 사이트에서 큰 O 표기법에 대한 연습을 할 때만 이 사실을 깨닫게 됩니다. 과장된 표현일 수도 있지만, 저도 이 두 가지가 가져다주는 우아함을 좋아하기 때문에 솔직히 말해서 이 두 가지를 많이 사용하지만, 제 요점은 이 두 가지가 전부는 아니라는 것입니다. 시퀀스, 람다, 리시버, 맵 축소 연산에 투자하면 성능에 불이익을 줄 수 있습니다. 그게 중요할까요? 중요할 때만 중요하므로 람다에 대한 전문가가 되라는 것이 제 최선의 조언입니다. 우리 모두는 람다와 리시버를 정말 좋아하지만, 때로는 좋은 오래된 것이 진정한 차이를 만들 수 있기 때문에 일상적인 코더의 삶에서 그것들이 분노의 포인트가 되지 않도록 하세요.

예를 들어 Java에서 정적 메서드보다 확장 함수가 더 낫다고 이야기하는 것도 좋은 관점은 아닙니다. 이러한 토론을 보거나 토론에 끌려 들어가면 한 쪽에서는 자신이 선택한 언어에 대해 매우 열정적이지만, 제가 보기에 실제로는 사람들이 자신의 개인적인 선호를 옹호하는 것일 뿐이라는 것을 알 수 있습니다. 저는 객관적인 태도를 취하는 것을 선호하는데, 어떤 언어에 대해서도 객관적으로 우려할 만한 점을 찾을 수 없습니다. 그냥 다를 뿐이죠. 그리고 그것은 훌륭합니다!

Java는 여러 면에서 Scala와 Kotlin의 부모입니다. Kotlin이 Java를 대체하기를 바라거나 바라는 것은 다소 무의미한 일이라고 생각합니다. 저는 개인적으로 모든 언어가 존재해야 하고 모든 언어에서 배워야 한다고 생각하는데, 서로 다르지만 결국 같은 일을 한다는 바로 그 원리가 우리를 활동적으로 유지하고 코드에 대한 다양한 관점을 이해하게 하는 원리와 정확히 같기 때문입니다. 저는 Java, Kotlin, Scala가 사라지는 것을 원하지 않습니다. 저는 이 모든 언어와 다른 언어도 함께 발전하기를 바랍니다. 그리고 저는 모든 언어에서 배우고 싶습니다. 제가 고무 키가 달린 ZX-Spectrum 48K 컴퓨터에서 테이프로 프로그래밍을 시작했던 거 기억하시나요? 제가 프로그래밍을 시작한 건 80년대 말이었죠. 지금과 비교하면 별 의미가 없겠지만, 과거와 현재에 어떤 문제가 있었고 앞으로 어떤 문제가 발생할 수 있는지를 더 잘 이해할 수 있게 해줍니다. 더 많은 언어가 세상에 가져다주는 풍요로움은 종종 간과되곤 합니다.

이 글에서 제가 정말 하고 싶은 말은 간단하고 명료합니다. 코루틴 구현도 그렇고, 코틀린은 새로운 플레이어입니다. 그리고 우리 모두는 이 둘을 좋아합니다. 하지만 어쨌든 저는 Java 가상 스레드와 관련하여 이러한 기술의 엔지니어링 부가가치를 보지 못했습니다. 제 생각에는 Kotlin은 단지 다르며 JVM에 새로운 풍미를 더한다고 생각합니다. 하지만 제가 Kotlin에 대해 알게 된 모든 비평가들이 Java에 대해서도 마찬가지라는 것을 알게 되었습니다. 마찬가지로, Kotlin에 대한 모든 칭찬을 Java에서도 똑같이 찾아볼 수 있습니다. 단지 스타일이 다를 뿐입니다. 물론 많은 기능이 Java SDK에 통합되어 있지 않지만, Kotlin은 여전히 JVM 위에 있는 DSL일 뿐입니다. 즉, Java에서 롬복과 같은 것을 사용한다고 해도 동일한 기능을 사용할 수 있을까요? Kotlin과 마찬가지로 또 다른 DSL일 뿐입니다. 이 글을 읽고 있는 많은 분들이 Lombok이 "끔찍한 아이디어"라고 말한다면 저는 "하지만 지금은 Java에 있습니다!"라고 말하고, "하지만 데이터 클래스가 이 모든 것을 함께 수행하고 모든 것을 불변으로 만들 수 있고 훨씬 더 좋아 보입니다!"라고 말할 것입니다. 모두 놀랍고 저도 마지막 말에 동의합니다. Kotlin이 더 좋아 보이긴 하죠. 그렇죠? 저는 주석을 사용하는 것을 선호할 수도 있고, 대신 을 사용하는 것을 선호할 수도 있고, 단일 데이터 키워드 뒤에 해시 구현, 같음, 게터 및 설정자가 있고, 모든 프로퍼티에 사용하면 변경 불가능한 개체가 있다는 것을 상기시키고 싶을 수도 있습니다! 바로 이 부분에서 저는 Kotlin이 천재적인 언어라고 생각합니다. 코틀린이 코드에 어떤 엔지니어링 이점을 제공하는지는 아직 명확하지 않지만, 우리의 본능과 최신 트렌드를 타고 오늘날 많은 개발자와 엔지니어가 직면하고 있는 인식의 격차를 메울 수 있는 절호의 기회를 찾았습니다. 상용구, 반복되는 코드, 어려운 코드, 엔지니어링 비용 등이 바로 그것입니다. 또한 구조화된 동시성을 보장하는 데 있어 놀라운 프로그래밍 스타일을 제공합니다. 물론 자극적이고 새로운 것을 하고자 하는 우리의 욕구도 있습니다. 새로운 구문과 새로운 의미론은 완전히 새로운 놀이터를 만들며, 이는 긍정적인 일입니다.

제가 보기에 Kotlin이나 Java 모두 엄격한 엔지니어링 측면에서 서로보다 낫다고 할 수 없습니다. 물론 동의하지 않을 수도 있습니다. 그리고 Android 배경을 가진 분이라면 제가 할 수 있는 말보다 훨씬 더 많은 이야기를 하실 수 있을 것 같습니다. 저는 Kotlin이 Android 개발자들에게 널리 사용되고 있다는 것을 잘 알고 있습니다. 좋은 소식인 것 같네요. 제 의견은 서비스 구현에만 국한된 관점에서 나온 것입니다. Android에는 훨씬 더 많은 것이 있으므로 이에 대한 언급은 삼가야겠습니다. 지금은 그렇습니다.

새로운 기술을 선택해야 한다면 가장 마음에 드는 기술을 선택하라고 조언하고 싶습니다. 언어 자체에서 성능상의 이점을 찾을 수 있을지는 의문입니다. 팀과도 보조를 맞춰야 합니다. 팀원들이 Kotlin에 대한 열정이 있다면 그쪽으로 가세요. Java에 대한 열정이 있다면 그렇게 하세요. 열정에서 가장 높은 생산성을 찾을 수 있습니다. 효율적인 것을 원하고 그것이 유일한 관심사라면, 그리고 이에 대해 매우 폭넓은 공감대가 형성되어 있다면 애초에 JVM과 관련된 모든 것을 멀리하는 것이 좋을 수 있습니다. JVM에서 작업을 시작하고 실행하는 것이 어려울 수 있으며, 이것이 많은 사람들이 네이티브 솔루션으로 전환하는 이유입니다. 또한 코루틴이 때때로 멀티스레딩과 더 많은 스레드 제공이라는 맥락에서 논의되는 경우가 있다는 점도 지적하고 싶습니다. 이는 사실이 아닙니다. 코루틴을 둘러싼 패러다임은 본질적으로 다른 어떤 것보다 반응형 프로그래밍과 더 관련이 있습니다. 이렇게 말하는 이유는 코루틴이 시스템/플랫폼 스레드를 훨씬 더 효율적으로 사용하기 때문입니다. 이것이 멀티스레딩과 관련이 있는 것처럼 들릴 수 있지만, 그렇지 않습니다. 이는 단지 예전처럼 스레드가 정당한 이유 없이 일시 중지되는 것을 방지하는 방법일 뿐입니다. Kotlin 코루틴을 사용할지, 아니면 곧 출시될 JDK19의 가상 스레드를 사용할지는 전적으로 사용자에게 달려 있습니다.

이 글을 쓰게 된 동기는 언젠가 루시 이야기에서 보여줄 것처럼, 때때로 우리는 서로에게 아주 좋은 이야기를 들려주지만 결국 아무 의미가 없는 경우가 있기 때문입니다. 저는 개인적으로 기분이 좋을 때 어떤 언어로든 프로그래밍을 계속할 것입니다. 일할 때는 계획대로 진행하죠. 하지만 여가 시간에는 그때그때 기분이 좋은 것을 선택하는데, 여기에는 Java, Kotlin, Scala, Go, Rust, Python, Ruby, PHP, Javascript 등이 포함됩니다.

서론에서 언급했듯이 이 글은 실험적인 성격을 띠고 있기 때문에 더 자주 검토될 것입니다.

이 애플리케이션의 모든 소스 코드는 GitLab에 배치했습니다.

제가 이 글을 쓰는 것만큼이나 여러분도 이 글을 재미있게 읽으셨기를 바랍니다.

여러분의 의견을 듣고 싶으니 아래에 의견을 남겨 주세요.

읽어주셔서 감사합니다!

참고

https://itnext.io/kotlin-coroutines-vs-java-virtual-threads-a-good-story-but-just-that-91038c7d21eb

http://gunsdevlog.blogspot.com/2020/09/java-project-loom-reactive-streams.html

https://www.baeldung.com/kotlin/java-kotlin-lightweight-concurrency

https://findstar.pe.kr/2023/04/17/java-virtual-threads-1/

profile
시간대비효율

0개의 댓글