Java 프로그래밍기초 230410 #15 쓰레드 / 프로세스

김춘복·2023년 4월 10일
0

Java 공부

목록 보기
18/20
post-custom-banner

쓰레드

프로세스와 쓰레드

프로세스(Process)

실행중인 프로그램.
프로그램을 실행하면 OS로부터 실행에 필요한 자원(Resource)을 할당받아 프로세스가 된다.
프로그램을 수행하는데 필요한 데이터와 메모리, CPU 등의 자원과 쓰레드로 구성되어있다.
흔히들 프로세스를 공장, 쓰레드를 일꾼에 비유한다.

쓰레드(Thread)

프로세스 내에서 실제로 작업을 수행한다.
모든 프로세스는 최소 하나 이상의 쓰레드가 존재한다.
경량 프로세스(LWP, Light-weight process)라 하기도 한다.
둘 이상의 쓰레드를 가진 프로세스는 멀티 쓰레드 프로세스(multi-threaded process)라 한다.

멀티쓰레딩

한 프로세스 내에서 일꾼이 여러 명이라 여러 작업을 나눠서 효율적으로 처리한다.
대부분의 프로그램은 멀티 쓰레드.
싱글 쓰레드 방식은 동시에 다른 일을 할 수 없다.

  • 장점 : 여러모로 좋다
    시스템 자원을 효율적으로 사용할 수 있다. 작업이 분리되어 코드가 간결해진다.
    사용자에 대한 응답성이 향상된다.
  • 단점 : 여러 쓰레드가 자원을 공유하는 데서 문제가 발생. 고려사항이 많아진다.
    동기화(synchronization)에 유의해야 한다.
    특정 쓰레드가 작업할 기회를 갖지 못하는 기아, 교착상태(deadlock)가 발생할 수 있다.
    각 쓰레드가 효율적으로 고르게 실행되어야 한다.

쓰레드의 구현과 실행

쓰레드 구현

  1. Thread 클래스를 상속
class myThread extends Thread {
	public void run() { // Thread 클래스의 run()을 오버라이딩
    // 쓰레드가 수행 할 작업내용
    }
}
  1. Runnable 인터페이스를 구현(오로지 run()만 정의되어 있다)
class myThread2 implements Runnable {
	public void run() { // Runnable 인터페이스의 추상메서드 run()을 구현
    // 쓰레드가 수행 할 작업내용
    }
}
  • 두 번째 방법이 재사용성이 높고 코드의 일관성을 유지할 수 있기 때문에 더 객체지향에 맞다.
    두 번째 방법으로 구현하기 위해선, 우선 Runnable 인터페이스를 구현한 클래스의 인터페이스를 생성한 다음 이 인스턴스를 Thread 클래스의 생성자의 매개변수로 제공해야 한다.
MyThread t1 = new MyThread(); // 1번 방법으로 쓰레드 생성
Runnable r = new MyThread2(); // 2번방법으로 Runnable 인터페이스 만들고
Thread t2 = new Thread(r); //  2번방법으로 Thread 클래스의 생성자에 위에껄 넣음
Thread t3 = new Thread(new MyThread()); // 2번 방법을 한줄로 표현

쓰레드 실행

start()를 호출해야만 쓰레드가 실행된다. ex) t1.start()
호출 되었다고 바로 실행되는건 아니고 실행 대기상태에 있다가 자신의 차례가 되면 실행된다.
실행 대기중인 쓰레드가 없으면 곧바로 실행된다.
실행이 종료된 쓰레드는 다시 실행할 수 없다. 하나의 쓰레드는 .start()가 한번만 호출된다.
두번 이상 호출하려면 쓰레드를 새로 생성해야 한다.

  • start()가 main 메서드에서 호출되면 start()는 새로운 쓰레드를 생성한다.
    새 쓰레드가 작업하는 데 사용 될 새로운 호출 스택을 생성한다.
    그 호출스택에서 run()이 호출되어 독립된 공간에서 작업을 수행한다.
    이렇게 되면 호출 스택이 2개가 되므로 스케쥴러가 정한 순서에 의해 번갈아가며 실행된다.

  • 쓰레드는 사용자 쓰레드와 데몬 쓰레드 두 종류가 있다.
    실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.


싱글 쓰레드와 멀티 쓰레드

  • 단순히 CPU만 사용하는 계산작업이면 멀티쓰레드보다 싱글쓰레드로 프로그래밍하는 것이 더 빠르고 효율적이다. 멀티쓰레드 환경에서는 쓰레드간 작업 전환(context switching)에 시간이 걸리기 때문이다.

  • CPU가 싱글코어인 경우에는 멀티 쓰레드라도 하나의 코어가 번갈아가면서 작업을 수행하므로 두 작업이 겹치지 않는다. 하지만 멀티코어라면 동시에 두 쓰레드가 수행될 수 있으므로 두 작업이 겹쳐진다.

  • 어떤 쓰레드를 얼마동안 실행할 것인지 결정하는 것은 OS의 프로세스 스케줄러가 결정한다. 그래서 쓰레드 실행 순서와 실행 시간은 일정하지 않다. Java가 OS 독립적이긴 하지만 OS 종속적인 부분이 몇개 있는데 쓰레드가 그 중 하나이다.

  • 두 쓰레드가 서로 다른 자원을 사용하는 작업은 멀티쓰레드 프로세스가 더 효율적이다. 사용자로부터 데이터를 입력받는 작업이나 네트워크로 파일을 주고받는 작업, 프린트 인쇄 등 처럼 외부기기와의 입출력을 필요로하는 경우가 이에 해당한다.

  • I/O 블락킹: 싱글 쓰레드 환경에서는 사용자로부터 입출력을 기다릴 때 작업이 중단된다.
    하지만 멀티 쓰레드에서는 사용자 입력을 기다릴 때 다른 작업을 할 수 있다.

우선순위

  • 쓰레드는 우선순위(priority)라는 멤버변수(속성)를 가진다. 이 우선순위에 따라 쓰레드가 얻는 실행시간이 달라진다. 작업의 중요도에 따라 우선순위를 나눠 특정 쓰레드가 더 많은 작업시간을 갖게할 수 있다.

  • 우선 순위의 범위는 1~10. 숫자가 높을수록 우선순위가 높다. main 쓰레드는 5. main에서 생성하는 쓰레드의 우선순위도 기본값은 5. .setPriority(7) 로 변경 가능

쓰레드 그룹

  • 서로 관련된 쓰레드를 그룹으로 다루기 위한 것. 보안상의 이유로 도입된 개념.
    Thread의 생성자를 이용하면 된다. 모든 쓰레드는 쓰레드 그룹에 포함되어 있어야 한다.
    지정을 하지 않고 만든다면 자신을 생성한 쓰레드와 그룹에 속한다.(기본값 main쓰레드 그룹)
    자바 어플리케이션이 실행되면 JVM은 main과 system이라는 쓰레드 그룹을 만든다.
    예를들어 가비지 컬렉션을 수행하는 Finalizer쓰레드는 system 쓰레드 그룹에 속한다.
    우리가 생성하는 모든 쓰레드 그룹은 main쓰레드 그룹의 하위 쓰레드 그룹이 된다.

데몬 쓰레드(deamon thread)

  • 일반 쓰레드(데몬쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드.
    일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제로 자동 종료.
    가비지 컬렉터, 워드프로세서의 자동저장, 자동화면갱신 등이 데몬쓰레드에 속한다.
    무한루프나 조건문을 이용해 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 하면 된다. 일반 쓰레드와 작성/실행방법이 같지만 start() 호출로 실행 전에 .setDeamon(true)를 호출해야 한다.

쓰레드의 상태

NEW : 생성되고 아직 start()가 호출되지 않은 실행 전 상태
RUNNABLE : 실행중 or 실행 가능한 상태
BLOCKED : 동기화 블럭에 의해서 일시정지된 상태(lock이 풀릴 때 까지 기다리는 상태)
WAITING : 쓰레드의 작업이 종료되진 않았지만 실행 가능하지 않은(unrunnable) 일시정지 상태
TIME_WAITING : 일시정지 시간이 지정된 경우
TERMINATED : 쓰레드의 작업이 종료 된 상태

  1. 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 Runnable 대기열(큐와 같은 구조)에 저장되어 차례가 올 때 까지 기다려야 한다.
  2. RUNNABLE 상태이다가 자신의 차례가 오면 run() 실행 상태가 된다.
  3. 주어진 실행 시간이 다 되거나 yield()를 만나면 다시 Runnable 대기열 뒤로 들어가고 다음 차례의 쓰레드가 실행된다.
  4. 실행 중 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지 상태가 될 수 있다.
  5. 지정된 일시정지 시간이 다 되거나(time-out), notify(),resume(),interrupt()가 호출되면 다시 Runnable 대기열 뒤로 들어가 자기 차례를 기다린다.
  6. 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.

스케쥴링

쓰레드 프로그래밍이 어려운 이유는 동기화와 스케쥴링 때문이다. 효율적인 멀티쓰레드 프로그램을 만들기 위해서는 쓰레드가 낭비없이 사용되도록 정교한 스케쥴링을 짜야한다.

  • 스케쥴링과 관련된 메서드
    sleep() : 지정된 시간(mili초, 천분의 1초)동안 쓰레드를 일시 정지. 지정된 시간이 지나면 다시 실행대기 상태로 돌아간다.
    join() : 다른 쓰레드가 끝날 때 까지 현재 쓰레드를 대기시킨다.
    interrupt() : sleep이나 join에 의해 일시정지 상태인 쓰레드를 바로 깨워 실행대기상태로 만든다.
    yield() : 실행중인 쓰레드를 다른 쓰레드에게 양보시키고 다시 실행대기 상태로 만든다.
    stop() : 쓰레드를 즉시 종료.
    suspend() : 쓰레드를 일시 정지.
    resume() : suspend()에 의해 일시정지된 쓰레드를 다시 실행 대기 상태로 만든다.
  • sleep으로 일시 정지가 된 쓰레드를 interrupt로 호출하면 InterruptedException이 발생해 즉시 잠에서 깨 실행상태가 된다. 그래서 sleep()은 항상 try-catch로 위의 예외를 처리해줘야 한다.

  • interrupt()는 쓰레드가 현재 블록되어 있는 상태에서 InterruptedException을 발생시켜 스레드를 중지시키는 역할을 한다. 즉시 중지하는 것이 아니라 쓰레드가 실행중인 작업이 끝날 때 까지 기다린 후 중지된다.

  • suspend()와 stop()은 쓰레드를 제어하는 가장 쉬운 방법이지만 교착상태(Deadlock)를 일으키기 쉬워 사용이 권장되지 않고 'deprecated'되었다.

동기화(synchronization)

멀티쓰레드 프로세스에서 여러 쓰레드가 같은 프로세스 내의 자원을 공유하기 때문에 서로의 작업에 영향을 주게 된다. 그래서 한 쓰레드가 특정 작업을 마치기 전 까지 다른 쓰레드에 의해 방해받지 않도록 해야하는데 방법이 임계영역(critical section)과 잠금(락,lock)이다.
공유 데이터를 임계영역으로 지정하고 lock을 획득 한 단 하나의 쓰레드만 이 영역에서 코드를 수행할 수 있게 한다. 해당 쓰레드가 작업을 완료하면 lock을 반납하면 다음 쓰레드가 임계영역에서 lock을 얻어 코드를 수행할 수 있다.
위 처럼 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 쓰레드의 동기화라고 한다.

synchronized

  • 가장 간단한 동기화 방법. 임계영역을 설정하는데 사용.
    public synchronized void sum() 처럼 메서드 전체를 임계영역으로 저장하거나
    synchronized(객체의 참조변수){..} 처럼 메서드 내 특정 영역을 임계영역으로 지정할 수 있다.
    가능하면 후자로 임계영역을 최소화 하는 것이 성능면에서 효율적이다.

  • 그리고 특정 쓰레드가 lock을 오래 보유하면 그 동안 다른 쓰레드가 lock을 기다리게 되므로 작업 속도가 느려진다. 동기화된 임계영역의 코드를 수행하다가 작업을 진행할 상황이 아니면 wait()을 호출해 일단 쓰레드가 락을 반납하고 기다리게 한 후 다른 쓰레드가 락을 얻게 한다. 그리고 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해 다시 작업을 진행하게 한다.
    wait()과 notify()는 동기화 블록(synchronized)내에서만 사용할 수 있다.

profile
Backend Dev / Data Engineer
post-custom-banner

0개의 댓글