스레드

황희윤·2023년 11월 25일

프로세스와 스레드

  • 프로그램(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의 사용권이 주어질 경우 지금까지 실행한 이후 시점부터 진행할 수 있기 때문이다. 만약 현재까지 실행한 결과를 저장하지 못했다면, 처음부터 다시 명령어를 수행하는 불상사를 겪었을 것이다.

  • 또한 우선 순위에 따라 다시 사용권을 받게 되면 반드시 전에 저장했던 상태를 유지하고 있어야 한다. 프로세스 간에는 엄격히 독립적으로 운영되어야 하며, 프로세스의 상태 정보는 다른 프로세스가 실행하고 오더라도 같아야 한다.

  • 사용권은 운영체제에서 정한 우선 순위에 따라 결정(스케줄링)되는데, 이 우선순위는 다양한 알고리즘에 의해서 결정되기 때문에 어느 프로세스가 먼저 실행될지 예측하기 어렵다.

다중 프로그래밍 장점

  1. CPU의 사용률 극대화
    • 특정 작업이 지연되어 응답성이 떨어지는 문제 해결 가능
    • 다른 프로세스가 무한정으로 대기가 길어지는 starvation 상태가 되는 걸 막는다.
    • 유휴 시간을 최소화 한다.
  2. 동시에 동작되는 효과를 볼 수 있다.
    • 여러 작업을 조금씩 번갈아 가면서 수행하므로 해당 프로세스는 조금 느릴 뿐, 자신만 수행하는것처럼 느끼게 되고, 전체적으로는 동시에 수행하는 것처럼 느껴진다.

다중 프로세스를 지원하는 운영체제의 문제점

  • 일반적으로 하나의 프로세스는 자신의 작업을 수행하기 위해 CPU 시간, 메모리, 파일, 입출력 장치 등 여러 자원을 요구한다.

  • 따라서 프로세스를 생성하고 유지하는데 비용이 많이 든다.

  • 다중 프로세스의 경우, 프로세스 간 스위칭(switching)이 일어나는데, 스위칭이 일어날 때마다 현재 상태를 저장을 해야 하고 관리해야 할 정보 값이 많아서 비용이 많이 든다. 그래서 만들어진 것이 경량화된 프로세스(light-weight process), 즉 스레드(thread)이다.


스레드 (Thread)

프로세스(process) 내에서 나뉜 하나의 작업 단위

  • 작업 단위 : 실행이 가능한 최소의 단위

  • 하나의 스레드에는 여러 스레드(작업)가 존재할 수 있다.

  • 다중 프로세스에서 각각의 스레드는 힙과 클래스(static) 변수, 코드(메서드 영역)을 공유하는 반면, 자기 고유의 레지스터와 스택(참조 변수)를 가지고 있다.

스레드의 활용

  • 메인(main) 스레드 : 프로세스가 생성되면 별도의 요청이 없어도 생성되는 스레드

단일 스레드의 순차적 작업 예시

  1. 메인 스레드에서 네트워크를 통해 전송받은 데이터를 처리
  2. UI 출력
  3. 이벤트 처리

단일 스레드 문제점과 다중 스레드

  • 네트워크의 경우 입출력(I/O)에 해당하는 작업으로 CPU는 유휴 시간발생한다.

  • 이와 같은 문제를 해결하기 위해 네트워크 처리는 하나의 스레드로 구성해서 처리하고, 나머지 작업은 별도의 스레드로 구성함으로써 동시에 수행하는 다중 스레드를 활용한다.

  • 네트워크 처리가 완료될 때까지 UI 작업 스레드에서는 "로딩 중..."이라는 메세지를 처리한다.

  • CPU의 유휴 시간은 최소화하고 CPU의 사용률은 최대화한다.


스레드의 생성과 구현

  • 스레드 = 독립적인 실행 단위 = 코드 + 개별 데이터 = 객체

  • 객체 = 코드(메서드) + 데이터(인스턴스 변수)

  • 스레드를 자바에서 생성하려면 객체 단위로 생성해야 한다.

  • 자바는 스레드와 관련해서 java.lang.Thread 클래스와 java.lang.Runnable 인터페이스를 제공한다.

스레드를 구현하는 두 가지 방법

1. Thread 클래스를 상속받아 재정의

class MyThread extends Thread {
	@override
    public void run ( ) {
    }
}
  • 자바는 단일 상속을 원칙으로 하기 때문에 이 방법은 독립적으로 실행되는 스레드를 생성하는 경우에 주로 사용한다.

2. Runnable 인터페이스를 상속받아 재정의

class MyThread implements Runnable {
	@override
    public void run ( ) {
    }
}
  • 다중 상속이 필요한 경우 Runnable 인터페이스를 사용한다.

스레드 생성 방법

1. Thread 클래스로 정의한 경우

class MyThread extends Thread {
	@override
    public void run( ) {
    	// 스레드를 사용해서 작업하려는 기능 재정의
    }
}

MyThread t = new MyThread(); // 스레드 생성
t.start();

2. Runnable 인터페이스로 정의한 경우

  • Runnable 인터페이스를 사용해서 스레드를 생성하려면 Thread 클래스의 생성자에 매개 변수로 지정해서 생성해야 한다.
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) 상태가 된다.

sleep() 메서드

  • sleep() 메서드가 호출되면 슬립 풀(sleep pool)로 들어가며 일시 중지 상태가 된다.
  • 이 메서드는 milisec 단위로 나타내는 매개 변수를 가질 수 있다.
  • 이 메서드에 의해 일시 중지된 스레드는 지정된 시간 동안 슬립 풀에서 대기한다.
  • 시간이 모두 지나거나 지정된 시간 전에 interrupt() 메서드가 호출되면 슬립 풀에서 빠져나올 수 있다. 그리고 빠져나온 스레드는 준비 상태가 되어 실행 대기 풀(runnable pool)로 들어간다.
  • 대부분 sleep() 메서드는 다른 스레드에게 CPU 사용권을 넘겨 실행 기회를 주기 위한 목적으로 사용한다.

join() 메서드

  • 현재 실행 중인 스레드에 대하여 join() 메서드가 호출되면 스레드는 적어도 milisec 동안 종료되지 않고 기다리며 대기(blocked) 상태가 된다.

  • 주어진 시간이 지나면 자동으로 준비(runnable) 상태가 된다.


스레드의 우선 순위

  • 우선 순위가 높은 스레드에 CPU 사용권을 먼저 주어 실행한다.

  • Thread 클래스에 정의된 setPriority() 메서드를 이용해 스레드에 우선 순위를 부여한다.

  • 스케줄러에는 준비(runnable) 상태에 있는 스레드만 우선 순위를 갖게 되며, 대기(blocked) 상태에 있는 스레드는 우선 순위가 없다.


스레드의 그룹

  • 서로 관련된 스레드를 그룹으로 묶어 한꺼번에 제어하는 기술

  • 모든 스레드는 반드시 하나의 그룹에 속해야 하고, 스레드 그룹을 지정하지 않고 생성된 스레드는 메인 스레드 그룹에 속한다.

  • 보안상 다른 스레드 그룹의 스레드는 변경할 수 없다.


스레드의 동기화

  • 다중 스레드 환경에서는 독립적인 자원이 아닌 공유 자원을 이용해 작업을 수행하는 경우가 있다.

  • 이럴 때 서로의 작업에 영향을 미칠 수도 있기 때문에, 공유 자원에 접근할 때는 한 번에 하나의 스레드만 접근할 수 있도록 잠금(lock)을 걸어 데이터의 일관성을 유지하는 기술을 동기화(synchronized)라고 한다.

스레드 동기화의 두 가지 관점

1. 실행 순서의 동기화

  • 실행 순서를 중요시 하여, 스레드가 이 순서에 반드시 따르도록 한다.

2. 메모리의 접근 동기화

  • 여러 스레드가 메모리에 접근할 때 동시 접근을 막는다.

임계 영역

  • 임계 영역 : 둘 이상의 스레드가 동시에 실행하면 문제가 발생하는 코드 블록

  • 메모리 접근 동기화는 임계 영역의 접근을 동기화하겠다는 뜻이다.

동기화로 인한 교착 상태

  • 교착 상태(deadlock) : 두 스레드가 잠금(lock)을 한 상태에서 서로 잠금이 풀리기를 기다리는 상황

스레드 간 조정

  • 생산자(producer) : 데이터를 생산

  • 소비자(consumer) : 데이터를 사용해서 작업 수행

  • 공유 객체 : 생산자와 소비자 사이에 상호작용하며 공유되는 객체

  • 생산 작업과 소비 작업을 동시에 수행할 수 있게끔 각각을 스레드로 구현한다.

  • 소비자는 생산자가 객체 생성을 완료할 때까지 기다려야 하고, 생산자는 소비자가 객체를 이용하는 작업을 완료할 때까지 기다려야 한다.

생산자 / 소비자 모델 구현 방법 두 가지

1. Busy wait 방식

  • 어떤 조건문을 만족할 때까지 무한 반복문을 돌면서 바쁘게 검사하는 방식

  • 반복물을 돌면서 소비자는 객체의 생성이 완료되었는지를 계속 확인한다.

  • CPU 낭비가 심하다.

2. 동기화 방식 ( 더 나은 방식)

  • 현재의 스레드를 일시 중지 시키고, 조건이 만족할 때 메시지를 사용해서 알려준다.

  • wait() 메서드가 호출되면 스레드는 잠금(lock)을 해제하고 실행을 일시적으로 중지한다.

  • 나중에 다른 스레드가 동일한 잠금을 얻어서 notify()notifyAll() 메서드를 호출하면 이벤트가 발생하기를 기다리면서 일시적으로 중지된 스레드들이 깨어난다.

  • wait() : 잠금(lock)을 양보하고 대기 상태로 들어간다.

  • notify() : 대기 상태인 스레드 중에서 하나의 스레드를 깨운다.

  • notifyAll() : 대기 상태인 스레드를 모두 깨운다.

  • 위의 모든 메서드들은 모두 Object 클래스에 소속되어 있다.

  • 위의 메서드들 모두 잠금(lock)에 관여하므로 synchronized 메서드에서만 사용할 수 있다.

  • wait() 메서드는 Thread 클래스의 suspend() 메서드와 비슷하다.

  • notify() 메서드는 Thread 클래스의 resume() 메서드와 비슷하다.

  • suspend()와 resume()을 사용하지 않는 이유는 deprecated 메서드로, 모니터링 잠금을 한 상태에서 스레드가 대기 상태로 바뀌기 때문에 교착 상태(deadlock)을 유발할 가능성이 있다.

profile
HeeYun's programming study

0개의 댓글