이전에 멀티쓰레드에 대하여 간단히 포스팅을 한 적이 있다.
예전에는 어떻게 사용하는지 예제를 들어서 코드로 구현을 했다면 이번 포스팅은 조금 더 이론적인 내용에 깊게 다가가려한다.
멀티 쓰레드를 이해하기 위해서는 먼저 프로세스와 쓰레드의 개념을 알아야한다.
프로세스란?
- 사용자가 실행한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행중인 것
- 자원(메모리, CPU, etc)와 쓰레드로 구성된다.
멀티 프로세스란?
말 그대로 여러개의 CPU로 여러 프로세스를 동시에 실행하는 것이다.
멀티 프로세스는 독립적인 메모리 공간을 가지고 따로따로 실행한다.
쓰레드란?
- 프로세스 내에서 실제 작업을 수행하는 일꾼이다.
- 모든 프로세스는 무조건 최소 하나의 쓰레드를 가지고 있다.
멀티 쓰레드란?
보통 하나의 프로세스는 하나의 쓰레드만을 가지고 작업을 한다.
하지만 멀티 쓰레드는 하나의 프로세스가 둘 이상의 쓰레드(일꾼)을 데리고 동시에 작업을 수행하는 것이다.
멀티 쓰레드는 자기가 속한 프로세스의 메모리를 공유한다
동시성Concurrency) 와 병렬성(Parallelism)
1. 동시성(ConCurrency)
- 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행되는 성질이다.
- 싱글 코어 CPU에서 멀티 쓰레드 작업을 수행할 시 병렬로 실행되는 것 처럼 보이지만 사실은 엄청난 속도로 양쪽을 번갈아가며 실행하는 동시성 작업이다. (너무 빨라서 느끼지 못하는 것)
2. 병렬성(Parallelism)
- 멀티 작업을 위해 여러개의 코어에서 개별 스레드를 동시에 수행하는 것이다.
(진짜 양쪽에서 같이 달리는 것)
멀티 쓰레드의 장점
프로그램의 전반적인 처리량 증가
예를 들어보자,
혼자서 밥을 하는 경우 : 재료 손질, 요리, 식탁 세팅 모든 걸 나눠서 혼자 해야한다.
세 명이서 밥을 하는 경우 : 각자 재료 손질, 요리, 식탁 세팅을 나눠서 하나 씩만 수행하면 된다.
모델링의 간소화
만약 싱글 쓰레드에서 위 내용을 코드로 해결하려면 비동기적으로 처리해야하는데 이것은 곧 코드가 복잡해지고 버그의 확률이 올라간다.
하지만 멀티 쓰레드라면 각 각 동기적인 작업으로 나누어 서로에게 필요한 경우에만 상호작용을 할 수 있게 되므로 버그의 확률이 낮아지고 유지보수가 용이해진다.
프로그램의 속도 상승
가끔 특정 홈페이지를 가면 무겁다는 느낌이 들 때가 있다.
예를들면 클릭이 늦게 먹는다던지 키보드의 반응이 늦는다던지.. 물론 이런 것들이 무조건 싱글 프로세스를 써서 그렇다! 는 아니다. 하지만 싱글 프로세스를 사용했다면 멀티프로세스를 사용했을 때 속도적인 이점을 얻을 수 있다.
멀티쓰레드의 단점
항상 좋은 점만 있는 것은 아니다.
예를들어 코드를 보자
public class UnsafeNextValue {
private int value;
public int next() {
return value++;
}
}
싱글 쓰레드에서는 문제없이 작동하지만 멀티 쓰레드로가면 문제가 생긴다.
동시에 진행하면서 낮은 확률로 중복된 값이 리턴 될 가능성이 있다.
이것을 동시성 문제라고 부른다.
이는 곧 프로그램의 신뢰성이 떨어지는 것이며 안정성이 떨어지게 된다.
하지만 이것도 해결할 방법이 있으니
public class SafeOperation {
@GuardedBy("this")
private int value;
public synchronized int next() {
return value++;
}
}
바로 자바에서 제공한느 synchronized를 메서드에 붙여준다.
위의 코드는 하나의 스레드만 연산이 실행하도록하는 동기화하는 방법이다.
synchronized를 사용해서 매우 간단하게 해결할 수 있지만 critical section의 크기 및 실행시간에 따라 성능하락 및 자원낭비가 매우매우 심해지게 된다.
해결 하는 방법으로 volatile과 Atomic 클래스도 있다고 한다. 하지만 이것은 다뤄보지 않았기에 나중 프로젝트에서 병렬프로그래밍을 할 때에 다뤄보고 따로 포스팅을 해보록 하겠습니다.
활동성 문제
이어서 또 다른 문제가 등장합니다. 안정성을 위해서 synchronized를 사용하게 되면 그 쓰레드는 락을 얻게 되는데 이렇게 되면 활동성 문제가 생기게 됩니다.
예 : (우리는 위에서 한 번에 하나만 실행하도록 synchronized 를 걸어놨기 때문에 다른 친구들은 '락' 이 걸려있는 상태)
데드락(Deadlock)
데드락은 하나의 예시를 들어 간단하게 설명할 수 있습니다.
식사 순서를 한 번 보면
- 일정시간 생각한다.
- 왼쪽 포크를 잡는다. 만약 왼쪽 포크가 사용중이라면 대기한다
- 이어서 오른쪽 포크를 집는다. 만약 오른쪽 포크가 사용중이라면 대기한다.
- 두 손에 있는 포크로 식사를 한다.
- 오른쪽 포크를 내려놓는다
- 왼쪽 포크를 내려놓는다
- 다시 1번으로 돌아간다.
여기서 모순이 발생합니다.
모든 철학자가 식사를 하기위해 3번 오른쪽 포크를 집는다. 를 수행할 경우 모든 사람이 동시에 오른쪽 포크가 사용중이라고 생각을 하고 대기하게 됩니다.
이렇게 될 경우 아무도 식사를 시작하지 못하고 대기만 하고 있는 상태, 즉 이 상황을 교착상태 = Deadlock 이라고 합니다.
라이브 락
그럼 데드락을 피하기 위해서 규칙을 하나 더 추가합니다.
오른쪽 포크를 잡지 못하는 경우 1분정도 기다렸다가 양보하기 위해 잡으려고 했던 왼쪽 포크에서 손을 때는 것 입니다.
이 경우 데드락은 발생하지 않지만 1분정도 기다렸다가 오른쪽 포크를 내려놓기 때문에 모든 철학자들이 왼쪽 포크를 집고, 1분동안 기다리는 상황을 무한대로 반복할 가능성이 있습니다. (골때리죠)
이 상황을 라이브 락 이라고 합니다.
기아 상태(starvation)
데드락과 라이브락 상황을 회피하기 위해서 작업의 우선순위를 줄 수도 있습니다.
예를들면 나이가 많은 어르신부터 식사를 시작하여 왼쪽과 오른쪽 포크를 집을 때 자신보다 나이가 어린 철학자가 포크를 집으려고 한다면 강제로 뺏는 조건입니다.
하지만 이런 경우 어린 철학자는 계속해서 포크를 빼앗기고 영원히 식사를 할 수 없게 될 가능성이 있습니다.
성심당에 빵 사려고 줄을 서있는데 나이 많은 사람이 계속해서 새치기하면 영원히 성심당에 들어갈 수 없겠죠?
이런 상황을 기아 상태 라고 합니다.
쓰레드의 개수에 따른 성능저하
쓰레드를 많이쓰면 무조건 이득일 것 같지만 아닙니다.
적정선에서 여러개 사용한다면 분명 이득이지만 일정 수준이상 많아지면 오히려 성능이 떨어질 수 있습니다.
운영체제는 쓰레드 단위로 CPU 자원에 대한 스케줄링을 합니다.
그럼 쓰레드는 스케줄링을 담당하는 스케줄러에 의해 CPU 자원을 획득하면 자신이 맡은 작업을 실행하겠죠?
이후 적당히 실행한 후 스케줄러가 다시 CPU자원을 내놓으라고 하면 자신이 실행하던 작업을 PCB(Process Controll Block)에 저장해둬야 합니다. 그래야 어디까지 했었고 어디서부터 이어서 할지 알 수 있겠죠
이 과정을 컨텍스트 스위칭이라고 하며 쓰레드가 많을 수록 컨텍스트 스위칭은 많이 일어나게 됩니다.
그럼 스케줄링에 쓰이는 시간이 늘어나고 CPU자원이 비효율적으로 굴러가면서 전체적인 성능 저하가 일어나는 것이죠.
마치며
이건 뭐 정리하면서도 이렇게 까다로운게 있나 싶지만,
그 만큼 중요한 구현방법이기에 잘 공부해서 이해하고 사용 해야하는 느낌이 듭니다.
그래야 장점은 가져가면서 단점과 위험성을 최소화 할 수 있으니까!