가상 스레드와 구조적 동시성에 관한 책
아직 ScopedValue, Task 등이 실험단계에 있어 API나 기능에 관한 설명은 제외하였다
- 자바 스레드
- 하나의 프로그램에서 가장 작은 독립적 실행 단위
- 기본 JVM 스레드 종류: 가비지 컬렉션 스레드, JIT 컴파일러 스레드, 시그널 디스패처 스레드, 소멸자 스레드, 참조 핸들러 스레드
- InterruptedException 을 catch 구문으로 잡을 경우 현재 스레드를 다시 인터럽트 상태로 유지하는 코드를 추가 해야 한다 → 호출 스택의 상부로 예외가 전달되지 않을 수 있다
- 자바 동시성 라이브러리 변천사
- 플랫폼 스레드
- 자바 스레드와 커널 스레드 간 1:1 대응 관계를 유지하는 스레드
- 자바의 커널(OS) 스레드의 추상화
- Executor
- 작업이 생성되면 큐에 저장되고 스레드 풀의 스레드 하나가 큐에서 작업을 가져와 수행
- Future.get 을 통한 결과 획득 과정에서 블록킹이 일어난다
- 캐시 일관성 유지에 어려움이 있어 거짓 공유 가능성이 존재한다
- core pool size: 대기 중이어도 풀 안에 유지되는 최소 스레드 수
- max pool size: 활성 스레드의 상한 수
- 종류
- fixed: 고정된 개수의 스레드 풀
- cached: 이전 스레드가 유휴 상태이면 재사용하고 없다면 생성
- single: 단일 스레드
- scheduled: 지연 시간 이후 일정한 간격으로 반복되는 작업을 수행
- work-stealing: work-stealing 알고리즘을 구현하여 동적으로 작업 부하를 분산
- Fork/Join Pool
- 스레드 별 로컬 큐의 작업를 수행하도록 되어 있어 캐스 미스를 최소화했다
- Work-Stealing 알고리즘을 구현하여 다른 스레드의 로컬 큐의 작업을 가져와 수행한다
- 이 때 작업을 가져오는 스레드는 대상 스레드 로컬 큐의 마지막 작업을 가져온다
- 분할 정복 방식으로 태스크를 처리하도록 설계되어 있다
- 서브 태스크를 대기하는 동안 다른 태스크를 가져와 처리 가능
- 비동기 모드로 생성하면 LIFO(후입선출) 대신 FIFO(선입선출) 로 이벤트나 트리거 처리에 적합하게 수행한다
- CompletabelFuture
- 비동기 작업 흐름을 조합하기 위한 자바의 지원 객체
- 내부적으로 Fork/Join Pool을 사용하며 비동기 작업을 더욱 쉽게 처리하기 위해 제공
- Reactive 프로그래밍
- 데이터 스트림, 비동기 이벤트 처리, 논 블록킹 연산에 초점
- 복잡하고 디버깅이 매우 어렵다
- 그리고 가상 스레드
- 가상 스레드
- 캐리어(플랫폼) 스레드 위에서 동작하는 가상 스레드
- 캐리어 스레드는 비동기 모드의 fork/join pool 을 사용하여 장점을 그대로 가져온다
- 가상 스레드 수행 도중 블록킹 연산을 만나면 플랫폼 스레드에 제어권을 반납한다
- 스택 프레임을 스레드 스택이 아닌 힙에 저장한다
- 공유자원 풀은 세마포어를 이용하여 관리하는 것이 좋다
- continuation
- 현재 실행 상태를 저장하고 나중에 다시 이어서 실행할 수 있는 기능
- 가상 스레드가 실행을 멈추었다가 다시 실행 가능하게 하는 매커니즘
- 반환 배리어: 함수 반환 지점에 주입된 작은 코드 조각으로 필요한 스택 프레임을 컨티뉴에이션 스택으로부터 복사해야 하는지 판단하는 기능
- 장점
- 논블로킹 + 동기식 코드 작성이 가능하게 한다 → 단순하고 읽기 좋
- 구조적 동시성을 지원한다
- 스레드보다 효율적으로 자원을 사용한다
- 단점
- CPU 집약적 작업에서는 효과가 거의 없다
- 네이티브, 외부 함수에 대해서는 제어권 반납이 동작하지 않는다
- synchronized는 jdk 25 이하에서는 제어권 반납이 동작하지 않는다
- ThreadLocal 변수는 엄청난 메모리 폭증을 유발 할 수 있다
- 구조적 동시성
- 여러 태스크를 하나의 단일 작업 단위로 취급할 수 있도록 하는 패러다임
- 장점
- 고아 스레드로 인한 자원의 낭비를 줄일 수 있음
- 통합적으로 오류 처리와 취소를 다룰 수 있음
- StructuredTaskScope
- 작업을 같은 그룹으로 묶어 생애 주기를 관리하기 위한 자바 인터페이스
- 서브 태스크는 스코프 소유자가 join을 호출하여 스코프가 합쳐진 후 subTasks.get을 통해 결과를 얻을 수 있다
- Joiner 객체를 통해 유연한 조인 정책 정의가 가능하다 → 필요하다면 커스텀 Joiner 객체 생성도 가능하다
- 메모리 일관성: 스코프 소유자가 서브 태스크 포크 전 수행하는 동작은 서브태스크가 수행하는 모든 작업보다 먼저 수행된다
- 중첩해 사용하여 트리 구조와 같은 서브 태스크 위계 구조를 만들 수 있다
- ScopedValue
- 값을 특정 스코프에 구조적 방식으로 바인딩하여 컨텍스트와 일관성을 유지하는 방법
- 프레임워크,라이브러리 구현 세부 사항에 결합되는 현상을 제거하여 파라미터 오염 문제에 대응 한다
- ThreadLocal의 단점인 가변성과 생애주기 문제를 해결한다
- 기본적으로 생성되는 시점의 Thread 에 바인딩되어 다른 스레드에서는 데이터를 볼 수 없다
- 단, StructuredTaskScope 안에서 사용될 때는 모든 자식 스레드가 자동으로 값을 상속받는다
- 가상 스레드 시대의 Reactive Java
- 비동기 데이터 스트림과 변경의 자동 전파에 중심을 둔 선언적 프로그래밍 패러다임
- 논 블록킹, 비동기, 이벤트 기반 아키텍처와 관련이 깊다
- 지속적으로 유입되는 데이터를 배압을 통해 효과적으로 관리 할 수 있다
- 이러한 경우 동기화와 상태 관리가 가상 스레드나 코루틴보다 쉽다
- Vs Kotlin Coroutine
- 유사한 의미
- 컨텍스트 전파: ScopedValue = CoroutineContext
- 구조적 동시성: StrcuturedTaskScpoe = CoroutineScope
- 경량 스레드: Virtual Thread = Coroutine
- 차이점
- 가상 스레드와 달리 취소 전파가 자동
- 복잡한 비동기 흐름, 반응형에 더 유용
- Thread 개념이 아닌 Continuation이 단위