클라이언트/서버 예제
: 간단한 클라이언트/서버 애플리케이션이 있다고 가정해보자. 서버는 소켓을 열어놓고 클라이언트의 연결을 기다린다. 클라이언트는 소켓에 연결해 요청을 보낸다.
: 단일 스레드로 환경에선 성능 개선의 요소가 제한적이다. 그렇다고 다중 스레드를 사용하면 무조건 성능이 향상될까? 먼저 오래걸리는 부분을 먼저 체크해야한다.
: 가능성은 다음 2가지다.
•
I/O - 소켓 사용, 데이터베이스 연결, 가상 메모리 스와핑 기다리기 등
•
프로세서 - 수치 계산, 가비지 컬렉션 등
: I/O 연산에 주로 시간을 보낸다고 하면 스레드를 추가해 성능을 향상시킬 수 있다.
: 프로세서 연산에서 시간을 보내는 프로그램은 스레드를 추가해도 빨라지지 않는다. CPU 사이클은 한계가 존재하기 때문이다.
가능한 실행 경로
: 다중 스레드 환경에서 공유 자원을 통한 연산을 처리할 때, Data Race가 발생하는 이유
: 원자적 연산이 아닌 경우, 발생할 가능성이 생긴다.
: 대표적인 원자적 연산으론 int 타입의 값을 할당할 때인데, 이는 32비트 값을 할당하는 연산은 하나이기 때문이다. long 타입의 값을 할당하는 연산자는 32비트 연산 2개로 나뉘어지므로 원자적 연산이 아니다.
라이브러리를 이해하라
: 애플리케이션 단계에서 직접 스레드를 제어한다면 executor 라이브러리 사용을 고려하라. 본인은 그런 모험을 즐기지 않으므로 패승..
•
Blocking, Non-Blocking
: 둘의 차이는 단순하다. 직접 제어할 수 없는 대상을 처리하는 방법에 따라 나뉘는데, 이를 작업의 제어권에 관한 개념이라고 한다. 대표적인 예로 I/O, 동기화 작업 등이 있다.
: 즉 다른 작업을 처리하는 주체에게 작업의 제어권을 위임한 후, 곧바로 다시 회수해 자신의 작업을 이어나가느냐 다른 작업의 주체의 작업이 종료되기를 기다리느냐에 있다.
◦
Blocking
: 직접 제어할 수 없는 대상의 작업이 제어권을 가져간 후, 해당 작업이 완료될 때까지 제어권을 반환하지 않는 것이다.
: Thread의 관점에선, 어떠한 객체에 책임을 위임한 작업이 종료될 때까지 계속 대기하며 하나의 Thread를 점유하는 것이다
◦
Non-Blocking
: 직접 제어할 수 없는 대상에게 제어권을 넘겨주었다가 대상 작업을 실행시키고 곧바로 제어권을 회수해 자신의 작업을 계속 진행하는 것이다.
: Thread의 관점에선 하나의 Thread가 여러 개의 I/O 처리를 수행할 수 있게 해준다. 이는 I/O의 경우, 각 I/O 컨트롤러가 처리해주기 때문이다.
•
Synchronous, Asynchronous
: 작업의 흐름에 대한 개념이다. 두 작업이 있을 때, 두 작업이 동기화되어있다는 것은 두 작업의 시작 시간과 종료 시간이 같거나 한 작업의 종료 시간이 다른 작업의 시작 시간임을 의미한다.
◦
Synchronous
: 작업을 수행하는 두 개 이상의 주체(함수, 어플리케이션 등)가 서로 동시에 수행하거나 동시에 끝나거나, 끝나는 동시에 시작할 때를 의미한다.
: 시작과 종료를 동시에 하거나 하나의 작업이 끝나는 동시에 다른 주체가 작업을 시작하면 이를 Synchronous 작업이라고 볼 수 있다.
◦
Asynchronous
: 작업을 수행하는 두 개 이상의 주체가 서로의 시작, 종료시간과는 관계없이 별도의 수행 시작/종료 시간을 가지는 것을 의미한다.
: 다른 주체가 하는 작업이 자신의 작업 시간과는 관계가 없다. 즉 관심이 없다.
•
Blcoking, Synchronous
: 현재 주체가 작업을 진행하던 도중, 다른 주체에게 필요한 작업을 요청하는 경우, 그 즉시 현재 주체의 제어권은 다른 주체에게 전가된다.
: 다른 주체가 해당 작업을 마무리하는 동안 현재 주체는 제어권이 없으므로 아무런 작업도 수행하지 않고 기다리다가 다른 주체의 작업이 종료되면 결과를 전달받아 자신의 작업을 재개한다.
•
Non-Blocking, Synchronous
: 현재 주체가 작업을 진행하던 도중, 다른 주체가 작업을 실행하게 되면, 현재 주체는 다른 주체와 상관없이 계속 작업을 이어나간다.
: 다만 현재 주체는 Polling을 통해 다른 주체의 작업이 종료되었는지를 계속 검사한다.
•
Non-Blocking, Asynchronous
: 현재 주체가 작업을 진행하던 도중, 다른 주체가 작업을 실행하게 되면, 현재 주체는 다른 주체와 상관없이 계속 작업을 이어나간다.
: 다만 다른 주체의 작업이 종료됨을 CallBack을 통해 알려주기 때문에 굳이 Polling을 통해 지속적으로 검사할 필요가 없다.
•
Blocking, Asynchronous
: 현재 주체가 작업을 진행하던 도중, 다른 주체가 작업을 실행하게 되면, 현재 주체는 멈춘다. 보통 잘 일어나진 않으나 모든 동기 작업을 처리한 후, 비동기 작업을 기다리는 것 같다.
: 다른 주체의 작업이 종료됨을 지속적으로 Polling을 통해 검사하다가 다른 주체가 끝나면 현재 주체의 남은 작업을 처리한다.
•
뮤텍스
: 상호 배제의 머릿글자를 따서 만들어진 단어로 임계구역을 사용하는 스레드 간의 실행시간을 서로 겹치지 않게하여 독립성을 보장해주는 기술이다.
: 프로세스 간에 동기화할 때 사용할 수 있다.
◦
프로세스들의 공유 리소스에 대한 접근을 조율하기 위해 Locking과 Unlocking을 사용한다.
◦
뮤텍스 객체를 두 스레드가 동시에 사용할 수 없다.
•
세마포어
: 프로세스 간의 시그널을 주고받기 위해 사용되는 정수 값이다. 리소스의 상태를 나타내는 카운터로 다음 세 가지 원자적 연산을 지원한다.
◦
initialize: 세마포어 초기화
◦
decrement: 프로세스를 블록시킬 수 있다.
◦
increment: 블록되었던 프로세스를 정상화 시킨다. 이 세마포어를 카운팅 또는 범용 세마포어라고 한다.
: 현재 세마포어의 값에 따라 운영체제는 프로세스가 자원을 사용할 수 있는지 없는지 결정한다.
: 자원이 다른 프로세스에 의해 점유중인 경우, 일정시간을 기다리게 된다. 프로세스가 자원을 사용하는 동안에는 세마포어 값을 변경함으로써 다른 프로세스들이 기다리게 한다.
데드락
: 데드락은 다음 4가지 조건을 만족한다.
1.
상호 배제(Mutual exclusion) : 동시에 사용해도 괜찮은 자원을 사용함으로써 해결 → 대부분은 동시에 자원을 사용하기가 어려움
2.
잠금 대기(Lock & Wait) : 각 자원을 점유하기 전에 확인하고 어느 하나라도 점유하지 못한다면 지금까지 점유한 자원을 반환하고 다시 시작 함으로써 해결 → 기아, 라이브락이 발생할 수 있음
3.
선점 불가(No Preemption) : 다른 스레드로부터 자원을 뺏어오는 방법으로 해결 → 모든 요청을 관리하기가 힘듬
4.
순환 대기(Circular Wait) : 자원에 접근하는 순서를 고정함으로써 해결 → 다양한 상황이 있기에 자원 사용 순서가 다를 가능성이 높음