스레드 제어와 생명 주기2

1. 인터럽트 - 플래그 변수 사용

특정 스레드 작업을 중간에 중단하려면 어떻게 해야할까?

플래그를 사용해 작업을 중단시켜 보자.

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class ThreadStopMainV1 {

    public static void main(String[] args) {
        MyTask task = new MyTask();
        Thread thread = new Thread(task, "work");
        thread.start();

        sleep(4000);
        log("작업 중단 지시 runFlag=false");
        task.runFlag = false;

    }

    static class MyTask implements Runnable {

        volatile boolean runFlag = true;

        @Override
        public void run() {
            while (runFlag) {
                log("작업 중");
                sleep(3000);
            }

            log("자원 정리");
            log("자원 종료");
        }
    }
}

문제점

위 코드는 기능적으로는 문제가 없지만, task.runFlag = false; 코드 이후에 즉각적으로 work 스레드가 반응하지 않는다.

그 이유는 이미 while 문 안에서 약 3초간 작업을 진행하고 있기 때문에, 작업이 완료된 후(3초가 지난 후) 플래그를 확인할 수 있기 때문이다.

2. 인터럽트 - 인터럽트 발생

인터럽트를 사용하면 WAITING, TIME_WAITING 같은 대기 상태의 스레드를 직접 깨워서, 작동하는 RUNNABLE 상태로 만들 수 있다. (BLOCKED 상태는 인터럽트에 반응하지 않는다..)

아래 코드를 살펴보자.

메인 쓰레드에서 MyTask 인터럽트 발생 시, TIME_WAITING 상태로 있던 해당 스레드가 RUNNABLE 상태로 변경되어 즉각적으로 코드가 실행되는 것을 확인할 수 있다.

인터럽트가 발생하면 해당 스레드에 InterruptedException 이 발생한다.

  • 참고로, interrup() 를 호출했다고 해서 즉시 InterruptedException 을 던지는 것은 아니다.

  • 오직 sleep() 처럼 내부에서 InterrupException 을 던질 때 예외가 발생한다.

이때 2가지 일이 발생한다.

  1. work 스레드는 TIMED_WAITING 상태에서 RUNNABLE 상태로 변경되고, InterruptedException 예외를 처리하면서 반복문을 탈출한다.

  2. work 스레드의 인터럽트 상태는 false 로 변경된다.

3. 인터럽트 - 이전 코드 개선1

그런데 앞선 코드에서는 아쉬운 부분이 있다.

아래 코드에서 인터럽트를 체크하지 않기 때문이다.

  • 만약 sleep() 메서드가 없었으면, 인터럽트를 확인하지 못할것이다.

while 문 조건에 인터럽트 상태를 확인한다면 조금 더 빨리 반응할 수 있을 것이다. (물론, 지금 예제에서는 큰 의미가 없다)

아래 코드를 살펴보자

아래 코드는 while 문 조건에 isInterupted() 사용해 실행에는 큰 문제는 없어 보인다.

하지만 큰 문제가 있는데, 바로 work 스레드의 인터럽트 상태가 true 로 유지된다는 점이다.

  • 앞서 sleep() 에서 InterruptException 가 발생한 경우 스레드의 인터럽트 상태는 false 가 된다.

  • 반면에 isInterrupted() 메서드는 인터럽트 상태를 바꾸지 않는다. 단순하게 인터럽트의 상태를 확인한다.

아래 코드와 같이 인터럽트 발생 후 자원을 정리해야 하는 과정이 있다면 자원을 정리하지 못하는 상황이 발생한다.

  • 자바에서 인터럽트 예외가 발생하면, 스레드의 인터럽트 상태를 다시 정상(false) 으로 돌리는 이유가 이런 이유 때문이다.

  • 스레드의 인터럽트 상태를 정상으로 돌리지 않으면 이후에도 계속 인터럽트가 발생하게 된다.

4. 인터럽트 - 이전 코드 개선2

Thread.interrupted() 코드를 사용하게 되면 단순히 인터럽트의 상태를 추가하는 것에 추가적으로 인터럽트 상태를 정상(false) 으로 돌려준다.

아래 코드를 살펴보자.

Thread.interrupted() 메서드 사용을 통해서 인터럽트 상태 체크 및 인터럽트 상태를 정상(false) 으로 변경하였기 때문에, 자원 정리가 문제 없이 실행된다.

5. yield - 양보하기

어떤 스레드를 얼마나 실행할지는 운영체제가 스케줄링을 통해 결정한다.

그런데 특정 스레드가 크게 바쁘지 않은 상황 이러서 다른 스레드에 CPU 실행 기회를 양보하고 싶을 수 있다.

이렇게 양보하면, 스케줄링 큐에 대기 중인 다른 스레드가 CPU 실행 기회를 더 빨리 얻을 수 있다.

아래 코드를 살펴보자.

여기서는 3가지 방식을 사용한다.

  • Empty : sleep(1), yield() 없이 호출한다. 운영체제의 스레드 스케줄링을 따른다.

  • sleep(1) : 특정 스레드를 잠시 쉬게한다.

    • RUNNABLE -> TIMEWAITING -> RUNNABLE(스케줄링 큐) -> RUNNABLE

  • yield() : 다른 스레드에 실행을 양보한다.

    • RUNNABLE -> RUNNABLE(스케줄링 큐) -> RUNNABLE

Empty

  • 특정 스레드가 쭉 수행된 다음에 다른 스레드가 수행되는 것을 확인할 수 있다.

sleep()

sleep(1) 을 사용해서 스레드의 상태를 1밀리초 동안 아주 잠깐 RUNNABLE -> TIME_WAITING 으로 변경한다. 이렇게 되면 스레드는 CPU 자원을 사용하지 않고, 실행 스케줄링에서 잠깐 제외된다.

1밀리초의 대기 시간 이후 다시 TIMED_WAITING -> RUNNABLE(스케줄링 큐) -> RUNNABLE 상태가 되면서 실행 스케줄링에 포함된다.

하지만, 이 방식은 RUNNABLE -> TIME_WAITING 로 변경되는 복잡한 과정을 거치고, 또 특정 시간만큼 스레드가 실행되지 않는 단점이 있다.

  • 만약, 양보할 스레드가 없다면, 차라리 나의 스레드를 더 실행하는 것이 나은 선택일 수 있다.

    (양보할 스레드가 없는데, 나혼자 쉬고있는 꼴이다.. )

yield()

자바 스레드가 RUNNABLE 상태 일 때, 운영체제의 스케줄링은 다음과 같은 상태들을 가질 수 있다.

  • 실행 상태(RUNNABLE) : 스레드가 CPU 에서 실제로 실행 중이다.

  • 실행 대기 상태(READY == RUNNABLE) : 스레드가 실행될 준비가 되었지만, CPU 가 바빠서 스케줄링 큐에서 대기 중이다.

운영체제는 실행 상태의 스레드들을 잠깐만 실행하고 실행 대기 상태로 만든다. 그리고 실행 대기 상태의 스레드들을 잠깐만 실행 상태로 변경해서 실행한다. 이 과정을 반복한다.

  • 참고로 자바에서는 두 상태를 구분할 수 없다.

yield() 의 작동

Thread.yield() 메서드는 현재 실행 중인 스레드가 CPU 스케쥴링 큐에 들어가 다른 스레드가 실행될 수 있도록 한다.

yield() 메서드를 호출한 스레드는 RUNNABLE 상태를 유지하면서 CPU 를 양보한다.

  • 즉, 이 스레드는 다시 스케줄링 큐에 들어가면서 다른 스레드에게 CPU 사용 기회를 넘긴다.

  • 참고로 yield() 는 운영체제 스케줄러에게 단지 힌트를 제공할 뿐, 강제적인 실행 순서를 지정하지 않는다.

  • 때문에, 반드시 다른 스레드가 실행되는 것도 아니다. 때문에, 양보할수도 하지 않을수도 있다.

yield()RUNNABLE 상태를 유지하기 때문에, 쉽게 이야기해서 양보할 사람이 없다면, 본인 스레드가 계속해서 실행될 것이다.

Last updated