절차지향으로 프로그래밍하는 거보다 동시에 실행하는 프로그램을 프로그래밍을 하는 이유가 무엇일까? 이 관점에서 자바 병렬 처리를 하는 이유를 살펴보자.
스레드는 자바 언어에서 피할 수 없는 특성이고 복잡한 비동기 코드를 더 단순한 순차적 코드로 바꿔 복잡한 시스템을 단순하게 개발할 수 있게 해준다.
게다가 스레드는 멀티프로세서 시스템의 능력을 최대한 끌어낼 수 있는 가장 쉬운 방법이다.
프로세서 개수가 늘어날수록 여러 작업을 동시에 실행하는 일은 매우 중요하다.
스레드는 각기 별도의 프로그램 카운터(PC), 스택 , 지역 변수를 갖는다. 또한, 프로그램을 스레드로 분리하면 멀티프로세서 시스템에서 자연스럽게 하드웨어 병렬성을 이용할 수 있다. 즉 한 프로그램 내 여러 스레드를 동시에 여러 개의 CPU에 할당해 실행시킬 수 있는 것이다.
스레드를 가벼운 프로세스라고 부르기도 하며, 현대 운영체제의 대부분은 프로세스가 아니라 스레드를 기본 단위로 CPU 자원의 스케줄을 정한다.
의도적으로 조율하지 않는 한 하나의 스레드는 다른 스레드와 상관없이 비동기적으로 실행된다.
스레드는 자신이 포함된 프로세스의 메모리 주소 공간을 공유하기 때문에 한 프로세스 내 모든 스레드는 같은 변수에 접근하고 힙에 객체를 할당한다.
이 때문에 프로세스 때보다 더 세밀한 단위로 데이터를 공유할 수 있다. 하지만 공유된 데이터에 접근하는 과정을 적절하게 비동기화하지 않으면 다른 스레드가 사용중인 변수를 순간적으로 수정해서 예상치 못한 결과를 얻을 수도 있다.
스레드를 제대로만 사용하면 개발 및 유지 보수 비용을 줄이고 복잡한 애플리케이션의 성능을 향상시킬 수 있다.
비동기적인 일 흐름을 거의 순차적으로 바꿀 수 있어 사람이 일하고 상호 작용하는 방식을 모델링하기 쉬워진다.
꼬인 코드를 새로 작성해 읽기 쉽고 유지 보수하기도 쉬운 명료한 코드로 만들 수도 있다.
GUI 애플리케이션에서 사용자 인터페이스가 더 빨리 반응하게 만들기도 하고, 서버 애플리케이션에서 자원 활용도와 처리율을 높이는데 유용하다.
JVM을 더 단순하게 구현할 수 있도록 도와준다. 가비지 컬렉터는 보통 하나 또는 두 개 이상의 전용 스레드에서 실행된다.
프로세서 스케줄링의 기본 단위는 스레드이기 때문에 스레드 하나로 동작하는 프로그램은 한 번에 최대 하나의 프로세서만 사용합니다.
프로세서가 두 개인 시스템에서 스레드가 하나뿐인 프로그램을 실행하면 CPU 자원의 50%를 낭비하는 셈입니다.
또, 프로세서가 100개인 경우라면 99%를 낭비하게 됩니다.
반면에 활성 상태인 스레드가 여러 개인 프로그램은 여러 프로세서에서 동시에 실행될 수 있습니다. 제대로 설계하기만 한다면 멀티스레드 프로그램은 가용한 프로세서 자원을 더 효율적으로 이용해서 처리 속도를 높일 수 있습니다.
여러 개의 스레드를 사용하면 프로세서가 하나라 해도 처리 속도를 높일 수 있습니다. 프로그램이 스레드 하나로 구성되면(단일 스레드) 동기 I/O 작업이 완료될 때까지 기다리는 동안 프로세서가 놀게 됩니다.
멀티스레드 프로그램에선 스레드 하나가 I/O가 끝나길 기다리는 동안 다른 스레드가 계속 실행될 수 있습니다. 즉 I/O 때문에 대기 상태에 들어가는 동안에도 다른 스레드는 동작할 수 있기 때문에 자원이 낭비되지 않습니다.
스레드는 서로 같은 메모리 주소 공간을 공유하고 동시에 실행되기 떄문에 다른 스레드가 사용 중일 지도 모르는 변수를 읽거나 수정할 수도 있습니다. 이는 상당히 편리한데, 다른 스레드간 통신 방식보다 데이터 공유가 훨씬 쉽기 때문입니다.
하지만 이 점은 위험 요소이기도 합니다. 즉 데이터가 예측 못한 시점에 변경돼 스레드가 혼동될 수도 있습니다. 여러 스레드가 같은 변수를 읽고 수정하게 되면 원래 순차적이던 프로그래밍 모델에 비순차적인 요소가 들어가 혼란스럽고 동작 과정을 추론하기 어려워질 수 있습니다.
멀티스레드 프로그램이 동작하는 모습을 예측하려면 스레드가 서로 간섭하지 않도록 공유된 변수에 접근하는 시점에 적절하게 조율해야 합니다. 다행히 자바에서는 공유 변수 접근을 조율하기 위한 여러가지 동기화 수단이 제공됩니다.
멀티스레드 환경에서는 데드락이 발생할 가능성이 있습니다. 예를 들어 A에서 스레드 B가 독점하고 있는 자원을 기다리고 있는데 스레드 B가 해당 자원을 절대 높지 않는다면, 스레드 A는 영영 기다리기만 할 것입니다.
또한, 성능 위험도 고려해야 합니다. 스레드를 사용하면 실행 중에 어느 정도 부하기 생기는 것이 사실입니다. 스레드가 많은 프로그램에서는 컨텍스트 스위칭(다른 스레드가 실행될 수 있게 스케줄러가 현재 실행중인 스레드를 잠시 멈출 때)이 빈번하고, 그 때문에 상당한 부담이 생깁니다.
즉, 실행중인 컨텍스트를 저장하고 다시 읽어들어야 하며, 메모리를 읽고 쓰는 데 있어 지역성이 손실되고, 스레드를 실행하기도 버거운 CPU 시간을 스케줄링하는데 소모해야 합니다. 또 스레드가 데이터를 공유할 떄는 동기화 수단도 사용해야 합니다. 이런 동기화는 컴파일러 최적화를 방해하고, 메모리 캐시를 지우거나 무효화하기도 합니다.
그 밖에 공유 메모리 버스에 동기화 관련 트레픽을 유발합니다. 이런 모든 요인은 성능 측면에서 추가적인 손실을 유발합니다.
여러 클라이언트 프로그램에서 소켓 연결을 받는 서버 애플리케이션의 경우 각 연결마다 스레드를 할당하고 동기 I/O를 사용하도록 하면 개발 작업이 쉬워집니다.
읽을 데이터가 없을 때 소켓에서 읽으려고 하면 애플리케이션은 추가 데이터가 들어올 때까지 read 연산에서 대기합니다. 이때 스레드가 하나뿐이라면 해당 요청에 대한 작업이 멈추는 것 뿐만 아니라 다른 모든 요청도 처리하지 못하고 멈춥니다.
이런 문제를 피하려면 단일 스레드 서버 프로그램의 경우에는 훨씬 복잡하고 실수하기도 쉬운 Non-Blocking I/O 기능을 써야만 합니다. (Node.js)
하지만 각 요청을 별개 스레드에서 처리하면(멀티 스레드) 대기 상태에 들어가도 다른 스레드가 요청을 처리하는데는 별 영향을 끼치지 않습니다.
표준 자바 API에도 대기 상태에 들어가지 않는 I/O를 지원할 수 있도록 java.nio 패키지가 있습니다.
https://junghyungil.tistory.com/198?category=892275