Application/JAVA & Kotlin
자바 병렬성과 동시성 정리
seungh0
2023. 3. 13. 00:15
반응형
- 병렬성: 한 테스크를 여러 하위 테스크로 나눠서 CPU의 다른 코어 또는 다른 머신에서 이들 하위 테스크를 병렬로 실행한다.
- 동시성: 조금씩 연관된 작업을 같은 CPU에서 동작하는 것 또는 애플리케이션의 생산성을 극대화할 수 있도록 코어를 바쁘게 유지하는 것이 목표라면, 원격 서비스나 데이터베이스 결과를 기다리는 스레드를 블록함으로써 연산 자원을 낭비하는 일을 피해야 한다.
자바는 이런 환경에서 사용할 수 있는 두 가지 주요 도구를 제공한다.
- Future 인터페이스로 자바 8의 CompletableFuture 구현은 간단하고 효율적인 문제 해결사.
- 자바9에 추가된 발행 구독 프로토콜에 기반한 리액티브 프로그래밍 개념을 따르는 Flow API는 조금 더 정교한 프로그래밍 접근 방법을 제공한다.
동시성은 단일 코어 머신에서 발생할 수 있는 프로그래밍 속성으로 실행이 서로 곂칠 수 있는 반면, 병렬성은 병렬 실행을 하드웨어 수준에서 지원.
동시성을 구현하는 자바 지원의 진화
- 처음에는 자바는 Runnable와 Thread를 동기화된 클래스와 메소드를 이용해 잠갔다.
- 자바 5는 좀 더 표현력있는 동시성을 지원하는 특히 스레드 실행과 테스크 제출을 분리하는 ExecutorService 인터페이스, 높은 수준의 결과, 즉 Runnable, Thread의 변형을 반환하는 Callable<T> and Future<T> 제네릭 등을 지원했다. ExecutorService는 Runnable와 Callable 둘 다 실행할 수 있다.
- 자바 7에서는 분할 그리고 정복 알고리즘의 포크/조인 구현을 지원하는 java.util.concurrent.RecursiveTask가 추가되었다.
- 자바 8에서는 스트림과 새로 추가된 람다 지원에 기반한 병렬 프로세싱이 추가되었다. 자바는 Future를 조합하는 기능을 추가하면서 동시성을 강화(Future 구현인 CompletableFuture)했다.
- 자바 9에서는 분산 비동기 프로그래밍을 명시적으로 지원한다. → 리액티브 프로그래밍이라 부르며 자바 9에서는 발행-구독 프로토콜 (java.util.concurrent.Flow) 인터페이스 추가로 이를 지원한다.
- CompletableFuture와 Flow의 궁극적인 목표는 가능한한 동시에 실행할 수 있는 독립적인 테스크를 가능하게 만들면서, 멀티 코어 또는 여러 기기를ㄷ통해 제공되는 병렬성을 쉽게 이용하는 것.
스레드와 높은 수준의 추상화
- 단일 CPU 컴퓨터도 여러 사용자를 지원할 수 있는데, 이는 운영체제가 각 사용자에게 프로세스 하나를 할당하기 때문이다.
- 운영체제는 두 사용자가 각각 자신만의 공간에 있다고 생각할 수 있도록 가상 주소 공간을 각각 프로세스에 제공한다.
- 운영체제는 주기적으로 번갈아가면서 각 프로세스에 CPU를 할당함으로써 프로세스는 다시 운영체제에 한 개이상의 스레드, 즉 본인이 가진 프로세스와 같은 주소 공간을 공유하는 프로세스를 요청함으로써 태스크를 동시에 또는 협력적으로 실행할 수 있다.
Executor와 스레드 풀
- 자바 5는 Executor 프레임워크와 스레드 풀을 통해 스레드의 힘을 높은 수준으로 끌어올리는 즉 자바 프로그래머가 태스크 제출과 실행을 분리할 수 있는 기능을 제공했다.
스레드 문제
- 자바 스레드는 직접 OS 스레드에 접근한다. OS 스레드를 만들고 종료하려면 비싼 비용 (페이지 테이블과 가관련된 상호작용)을 치러야 하며 더욱이 운영체제 스레드의 숫자는 제한되어 있는 것이 문제다.
- 운영체제가 지원하는 스레드 수를 초과해 사용하면 자바 애플리케이션이 예상치 못한 방식으로 크래시될 수 있으므로 기존 스레드가 실행되는 상태에서 계속 새로운 스레드를 만드는 상황이 일어나지 않도록 주의해야한다.
스레드 풀 그리고 스레드 풀이 더 좋은 이유
- ExecutorServic의 newFixedThreadPool 같은 팩토리 메소드 중 하나를 이용해 스레드 풀을 만들어 사용할 수 있다.
- 스레드 풀에서 사용하지 않은 스레드로 제출된 테스크를 먼저 온 순서대로 실행한다. 이들 태스크 실행이 종료되면 이들 스레드를 풀로 반환한다.
- 이 방식의 장점은 하드웨어에 맞는 수의 태스크를 유지함과 동시에 수 천개의 태스크를 스레드 풀에 아무 오버헤드 없이 제출할 수 있다는 점이다.
- 큐의 크기 조정, 거부 정책, 태스크 종류에 따른 우선순위 등 다양한 설정을 할 수 있다.
- 프로그래머는 태스크(Runnable or Callable)을 제공하면 스레드가 이를 실행한다.
스레드 풀 그리고 스레드 풀이 나쁜 이유
- 거의 모든 관점에서 스레드를 직접 사용하는 것보다 스레드 풀을 이용하는 것이 바람직하지만 두 가지 사항을 주의해야 한다.
- k 스레드를 가진 스레드 풀은 오직 k만큼의 스레드를 동시에 실행할 수 있다. 초과로 제출된 태스크는 큐에 저장되며 이전에 태스크 중 하나가 종료되기 전까지는 스레드에 할당하지 않는다.
- 불필요하게 많은 스레드를 만드는 일을 피할 수 있으므로 보통 이 상황은 아무 문제가 되지 않지만 잠을 자거나 I/O를 기다리는 블록 상황에서 이들 태스크가 워커 스레드에 할당된 상태를 유지하지만 아무 작업도 하지 않게 된다.
- 핵심은 블록할 수 있는 태스크는 스레드 풀에 제출하지 말아야 한다는 것이지만 항상 이를 지킬 수 있는 것은 아니다.
- 중요한 코드를 실행하는 스레드가 죽는 일이 발생하지 않도록 보통 자바 프로그램은 main이 반환되기 전에 모든 스레드의 작업이 끝나길 기다린다. 따라서 프로그램을 종료하기전에 모든 스레들 풀을 종료하는 습관을 갖는 것이 중요하다.
- k 스레드를 가진 스레드 풀은 오직 k만큼의 스레드를 동시에 실행할 수 있다. 초과로 제출된 태스크는 큐에 저장되며 이전에 태스크 중 하나가 종료되기 전까지는 스레드에 할당하지 않는다.
스레드의 다른 추상화: 중첩되지 않은 메소드 호출
- 비동기 메소드: 메소드 호출자에 기능을 제공하도록 메소드가 반환된 후에도 만들어진 태스크가 실행되는 메소드
- 이들 메소드를 사용할 때 어떤 위험성이 따르는가?
- 스레드 실행은 메소드를 호출한 다음에 코드와 동시에 실행되므로 데이터 경쟁 문제를 일으키지 않도록 주의해야 한다.
- 기존 실행 중이던 스레드가 종료되지 않은 상황에서 자바의 main() 메소드가 반환하면 어떻게 될까? 두 가지 방벙비 있는데 어느 방법도 안전하지 못하다.
- 애플리케이션을 종료하지 못하고 모든 스레드가 실행을 끝낼 때 까지 기다린다.
- 애플리케이션 종료를 방해하는 스레드를 강제종료시키고 애플리케이션을 종료한다.
- 첫 번째 방법에서는 잊고서 종료하지 못한 스레드에 의해 애플리케이션이 크래시될 수 있다. 또 다른 문제로 디스크에 쓰기 I/O 작업을 시도하는 일련의 작업을 중단했을 때 이로 인해 외부 데이터의 일관성이 파괴될 수 있다. 이들 문제를 피하려면 애플리케이션에서 만든 모든 스레드를 추적하고 애플리케이션이 종료하기 전에 스레드 풀에 모든 스레드를 종료하는 것이 좋다.
- 자바 스레드는 setDaemon() 메소드를 이용해 데모 또는 비데몬으로 구분시킬 수 있다.
- 데몬 스레드는 애플리케이션이 종료될 때 강제 종료되므로 디스크의 데이터 일관성을 파괴하지 않는 동작을 수행할 때 유용하게 활용될 수 있는 반면, main() 메소드는 모든 비데몬 스레드가 종료될 때 까지 프로그램을 종료하지 않고 기다린다.
- 이들 메소드를 사용할 때 어떤 위험성이 따르는가?
반응형