프로그램(program) : 명령어들의 집합
명령어들이 실행되려면 주기억 장치에 적재(loading) 되어야 한다.
프로세스(process) : 명령어들이 주기억 장치에 적재되어서 실행 가능한 상태가 된 프로그램
초기 시스템에서는 한 프로그램(=프로세스)이 주기억 장치에 적재되어 CPU를 독점해서 사용했었다. 그래서 음악을 듣다가 인터넷을 하려면 음악 듣기를 끝내야 했다.
이러한 처리 방식에서는 프로그램 내에서 입출력 명령과 같은 명령을 요청받으면 CPU는 해당 장치에 제어 신호만 보내고, 입출력이 끝날 때까지 아무런 작업을 하지 않는 유휴 시간(idle time)이 생겼다.
그 후 CPU와 주변 장치의 속도 차이가 점차 벌어짐에 따라 CPU가 기다리는 시간이 많아졌고 CPU의 사용률이 더욱 떨어졌다.
즉, 초기 시스템에서는 작업 하나만 메모리에 적재되었고, 그래서 CPU에 유휴 시간이 생겼을 때 다른 작업을 할 수 없어 CPU 사용률이 낮았다.
CPU의 유휴 시간을 활용하기 위해서 여러 개의 프로그램을 메모리에 적재하고, 한 프로세스가 CPU를 할당받아 실행한 후 주어진 시간이 종료되거나 입출력 요구가 발생하면 운영체제가 CPU의 사용권을 다른 프로세스로 넘겨줌으로써 CPU를 최대한으로 사용하는 기술이다.
1개의 CPU에 N개의 프로세스
초기 단일 프로세스에는 실행 중에 다른 프로세스에서 사용권을 가져갈 수 없는 비선점(non-preemptive) 원리가 적용되었다. CPU에 유휴 시간이 발생하더라도 CPU는 다른 작업을 할 수 없었다.
이 후 정해진 시간(time slice)이 지나면 다른 프로세스에 CPU의 사용권을 넘기는 시분할 시스템이 도입된다.
만약 시간이 부족해 끝내지 못하면 현재까지 실행한 결과를 저장하고 다른 프로세스에 CPU 사용권을 넘겨야 한다. 이는 다음번에 CPU의 사용권이 주어질 경우 지금까지 실행한 이후 시점부터 진행할 수 있기 때문이다. 만약 현재까지 실행한 결과를 저장하지 못했다면, 처음부터 다시 명령어를 수행하는 불상사를 겪었을 것이다.
또한 우선 순위에 따라 다시 사용권을 받게 되면 반드시 전에 저장했던 상태를 유지하고 있어야 한다. 프로세스 간에는 엄격히 독립적으로 운영되어야 하며, 프로세스의 상태 정보는 다른 프로세스가 실행하고 오더라도 같아야 한다.
사용권은 운영체제에서 정한 우선 순위에 따라 결정(스케줄링)되는데, 이 우선순위는 다양한 알고리즘에 의해서 결정되기 때문에 어느 프로세스가 먼저 실행될지 예측하기 어렵다.
일반적으로 하나의 프로세스는 자신의 작업을 수행하기 위해 CPU 시간, 메모리, 파일, 입출력 장치 등 여러 자원을 요구한다.
따라서 프로세스를 생성하고 유지하는데 비용이 많이 든다.
다중 프로세스의 경우, 프로세스 간 스위칭(switching)이 일어나는데, 스위칭이 일어날 때마다 현재 상태를 저장을 해야 하고 관리해야 할 정보 값이 많아서 비용이 많이 든다. 그래서 만들어진 것이 경량화된 프로세스(light-weight process), 즉 스레드(thread)이다.
프로세스(process) 내에서 나뉜 하나의 작업 단위
작업 단위 : 실행이 가능한 최소의 단위
하나의 스레드에는 여러 스레드(작업)가 존재할 수 있다.
다중 프로세스에서 각각의 스레드는 힙과 클래스(static) 변수, 코드(메서드 영역)을 공유하는 반면, 자기 고유의 레지스터와 스택(참조 변수)를 가지고 있다.
네트워크의 경우 입출력(I/O)에 해당하는 작업으로 CPU는 유휴 시간이 발생한다.
이와 같은 문제를 해결하기 위해 네트워크 처리는 하나의 스레드로 구성해서 처리하고, 나머지 작업은 별도의 스레드로 구성함으로써 동시에 수행하는 다중 스레드를 활용한다.
네트워크 처리가 완료될 때까지 UI 작업 스레드에서는 "로딩 중..."이라는 메세지를 처리한다.
CPU의 유휴 시간은 최소화하고 CPU의 사용률은 최대화한다.
스레드 = 독립적인 실행 단위 = 코드 + 개별 데이터 = 객체
객체 = 코드(메서드) + 데이터(인스턴스 변수)
스레드를 자바에서 생성하려면 객체 단위로 생성해야 한다.
자바는 스레드와 관련해서 java.lang.Thread 클래스와 java.lang.Runnable 인터페이스를 제공한다.
class MyThread extends Thread {
@override
public void run ( ) {
}
}
class MyThread implements Runnable {
@override
public void run ( ) {
}
}
class MyThread extends Thread {
@override
public void run( ) {
// 스레드를 사용해서 작업하려는 기능 재정의
}
}
MyThread t = new MyThread(); // 스레드 생성
t.start();
class MyRunnable implements Runnable {
@override
public void run() {
// 스레드를 사용해서 작업하려는 기능을 재정의
}
}
MyRunnable r = new MyRunnable(); // 스레드 생성
Thread t = new Thread(r);
t.start();
new 상태(생성 상태) : Thread 객체가 메모리에 생성만 된 상태
ready 상태(준비 상태) : 실행 대기 풀(runnable pool)에서 실행(running) 상태로 들어가려고 준비하는 상태
running 상태(실행 상태) : 스레드가 CPU를 독점해 코드를 실행 중인 상태
blocked 상태(대기 상태) : 이 상태에 있는 스레드는 일시적으로 스레드의 수행이 중단된 상태로서 특정 조건이 되면 다시 실행 상태로 이전된다.
exit 상태(종료) : 스레드가 정상적으로 종료된 상태
운영체제의 스케줄러가 실행 대기 풀(runnable pool)에서 대기 중인 스레드 하나를 선택하면 해당 스레드는 CPU를 독점하는 실행(running) 상태로 전환된다. 해당 스레드는 디스패치(dispatch)되었다고 말한다.
start 메서드를 사용해 준비(ready) 상태로 전환할 수 있는데, start 메서드가 호출되었다고 해서 바로 스레드가 실행되는 것은 아니다. 일단 대기(blocked) 상태로 있다가 스케줄러에 의해 순서가 결정된다.
스케줄러에 의해 디스패치된 스레드는 running 상태가 되면서 run 메서드를 호출한다.
스케줄러는 아직 종료되지 않은 스레드들의 우선 순위를 고려해 실행 순서와 실행 시간을 결정하고 디스패치되면 해당 스레드는 지정된 시간 동안만 작업을 수행한다.
이 때 시간 안에 작업을 종료하지 못한 스레드는 대기(blocked) 상태로 전환되고, 다시 자신의 차례가 될 때까지 준비(ready) 상태로 들어간다.
yield() : 현재 실행 중인 스레드를 일시적으로 중단 - 준비(runnable) 상태
sleep() : 현재 실행 중인 스레드를 지정된 시간 동안 수면 상태(일시적인 실행 중단)로 전환한다. 지정된 시간이 지나면 자동으로 준비(runnable) 상태가 된다.
stop() : 강제 종료
destroy() : 종료 처리를 하지 않고, 스레드를 버린다.
suspend() : 현재 실행 중인 스레드를 일시적으로 중단한다. sleep()과 비슷하지만, 시간을 단위로 중단시키는 것이 아니라 suspend() 메서드로 중단 시킨 후 resume() 메서드를 이용해 대기 상태로 바꾼다. 그러나 이들 메서드는 Dead Lock을 만들 가능성이 있어서 사용을 자제 해야 한다.
resume() : 다시 준비(runnable) 상태가 된다.
현재 실행 중인 스레드에 대하여 join() 메서드가 호출되면 스레드는 적어도 milisec 동안 종료되지 않고 기다리며 대기(blocked) 상태가 된다.
주어진 시간이 지나면 자동으로 준비(runnable) 상태가 된다.
우선 순위가 높은 스레드에 CPU 사용권을 먼저 주어 실행한다.
Thread 클래스에 정의된 setPriority() 메서드를 이용해 스레드에 우선 순위를 부여한다.
스케줄러에는 준비(runnable) 상태에 있는 스레드만 우선 순위를 갖게 되며, 대기(blocked) 상태에 있는 스레드는 우선 순위가 없다.
서로 관련된 스레드를 그룹으로 묶어 한꺼번에 제어하는 기술
모든 스레드는 반드시 하나의 그룹에 속해야 하고, 스레드 그룹을 지정하지 않고 생성된 스레드는 메인 스레드 그룹에 속한다.
보안상 다른 스레드 그룹의 스레드는 변경할 수 없다.
다중 스레드 환경에서는 독립적인 자원이 아닌 공유 자원을 이용해 작업을 수행하는 경우가 있다.
이럴 때 서로의 작업에 영향을 미칠 수도 있기 때문에, 공유 자원에 접근할 때는 한 번에 하나의 스레드만 접근할 수 있도록 잠금(lock)을 걸어 데이터의 일관성을 유지하는 기술을 동기화(synchronized)라고 한다.
임계 영역 : 둘 이상의 스레드가 동시에 실행하면 문제가 발생하는 코드 블록
메모리 접근 동기화는 임계 영역의 접근을 동기화하겠다는 뜻이다.
생산자(producer) : 데이터를 생산
소비자(consumer) : 데이터를 사용해서 작업 수행
공유 객체 : 생산자와 소비자 사이에 상호작용하며 공유되는 객체
생산 작업과 소비 작업을 동시에 수행할 수 있게끔 각각을 스레드로 구현한다.
소비자는 생산자가 객체 생성을 완료할 때까지 기다려야 하고, 생산자는 소비자가 객체를 이용하는 작업을 완료할 때까지 기다려야 한다.
어떤 조건문을 만족할 때까지 무한 반복문을 돌면서 바쁘게 검사하는 방식
반복물을 돌면서 소비자는 객체의 생성이 완료되었는지를 계속 확인한다.
CPU 낭비가 심하다.
현재의 스레드를 일시 중지 시키고, 조건이 만족할 때 메시지를 사용해서 알려준다.
wait() 메서드가 호출되면 스레드는 잠금(lock)을 해제하고 실행을 일시적으로 중지한다.
나중에 다른 스레드가 동일한 잠금을 얻어서 notify()나 notifyAll() 메서드를 호출하면 이벤트가 발생하기를 기다리면서 일시적으로 중지된 스레드들이 깨어난다.
wait() : 잠금(lock)을 양보하고 대기 상태로 들어간다.
notify() : 대기 상태인 스레드 중에서 하나의 스레드를 깨운다.
notifyAll() : 대기 상태인 스레드를 모두 깨운다.
위의 모든 메서드들은 모두 Object 클래스에 소속되어 있다.
위의 메서드들 모두 잠금(lock)에 관여하므로 synchronized 메서드에서만 사용할 수 있다.
wait() 메서드는 Thread 클래스의 suspend() 메서드와 비슷하다.
notify() 메서드는 Thread 클래스의 resume() 메서드와 비슷하다.
suspend()와 resume()을 사용하지 않는 이유는 deprecated 메서드로, 모니터링 잠금을 한 상태에서 스레드가 대기 상태로 바뀌기 때문에 교착 상태(deadlock)을 유발할 가능성이 있다.