멀티스레드와 동시성

Ch03. 스레드 제어와 생명 주기 - Join

webmaster 2024. 8. 4. 20:09
728x90

Waiting (대기 상태): 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태.

스레드로 특정 작업 수행
실행 결과

실행 결과 분석 - 시간의 흐름

  • 그림에서 생략되었지만, "Thread-2"도 "main" 스레드가 생성하고 start()를 호출해서 실행한다.
  • "thread-1" , "thread-2"는 각각 특정 작업을 수행한다. 작업 수행에 약 2초 정도가 걸린다고 가정하기 위해 "sleep()"을 사용해서 2초간 대기한다. (그림에서는 "RUNNABLE"로 표현했지만, 실제로는 "TIMED_WAITING" 상태이다.)
  • 실행 결과를 보면 "main" 스레드가 먼저 종료되고, 그다음에 "thread-1" , "thread-2"가 종료된다.
  • "main" 스레드는 "thread-1" , "thread-2"를 실행하고 바로 자신의 다음 코드를 실행한다. 여기서 핵심은 "main" 스레드가 "thread-1" , "thread-2"가 끝날 때까지 기다리지 않는다는 점이다. "main" 스레드는 단지 "start()"를 호출해서 다른 스레드를 실행만 하고 바로 자신의 다음 코드를 실행한다.
    • 그런데 만약 "thread-1" , "thread-2"가 종료된 다음에 "main" 스레드를 가장 마지막에 종료하려면 어떻게 해야 할까?
    • 예를 들어서 "main" 스레드가 "thread-1" , "thread-2"에 각각 어떤 작업을 지시하고, 그 결과를 받아서 처리하고 싶다면 어떻게 해야 할까?

Join이 필요한 상황

EX) "1 ~ 100"까지 더한결과는 "5050"이다. 이 연산은 다음과 같이 둘로 나눌 수 있다.

  • "1 ~ 50"까지 더하기 = "1275"
  • "51 ~ 100"까지 더하기 = "3775"
  • 두 계산 결과를 합하면 "5050"이  나온다.

"main" 스레드가 "1 ~ 100"으로 더하는 작업을 "thread-1" , "thread-2"에 각각 작업을 나누어 지시하면 CPU 코어를 더 효율적으로 활용할 수 있다. CPU 코어가 2개라면 이론적으로 연산 속도가 2배 빨라진다.

  • "thread-1" : "1 ~ 50" 까지 더하기
  • "thread-2" : "51 ~ 100" 까지 더하기
  • "main" : 두 스레드의 계산 결과를 받아서 합치기(이건 간단한 연산 한 번이니 속도 계산에서 제외하자)

1~50 / 51~100 까지 연산을 각자하는 스레드 작업 코드
실행결과

  • "SumTask" 는 계산의 시작값(startValue)과 계산의 마지막 값(endValue)을 가진다. 그리고 계산이 끝나면 그 결과를 result 필드에 담아둔다. main 스레드는 "thread-1" , "thread-2" 를 만들고 다음과 같이 작업을 할당한다.
    • thread-1 : "task1" - 1 ~ 50 까지 더하기
    • thread-2 : "taks2" - 51 ~ 100 까지 더하기
  • "thread-1"task1 인스턴스의 run() 을 실행하고, "thread-2"task2 인스턴스의 run()을 실행한다. 각각의 스레드는 계산 결과를 result 멤버 변수에 보관한다.
  • run()에서 수행하는 계산이 2초 정도는 걸리는 복잡한 계산이라고 가정하자. 그래서 sleep(2000)으로 설정했다. 여기서는 약 2초 후에 계산이 완료되고 result에 결과가 담긴다.
  • "main" 스레드는 "thread1" , "thread2" 에 작업을 지시한 다음에 작업의 결과인 task1.result , task2.result를 얻어서 사용한다.

실행 결과를 보면 기대와 다르게 task1.result , task2.result 모두 0으로 나온다. 그리고 task1 + task2의 결과도 0으로 나온다. 계산이 전혀 진행되지 않았다. 이 부분을 자세히 분석해 보자..

실행 결과 분석 - 시간의 흐름

실행 결과 분석 - 시간의 흐름

"main" 스레드는 "thread-1", "thread2"에 작업을 지시하고, "thread-1", "thread2"가 계산을 완료하기도 전에 먼저 계산 결과를 조회했다. 참고로 "thread-1", "thread-2"가 계산을 완료하는 데는 2초 정도의 시간이 걸린다. 따라서 결과가 task1 + task2 = 0으로 출력된다.

실행 결과 분석 - 메모리 구조

실행 결과 분석 - 메모리 구조 1

 

  • 프로그램이 처음 시작되면 main 스레드는 thread-1 , thread-2 를 생성하고 start() 로 실행한다.
  • "thread-1" , "thread-2"는 각각 자신에게 전달된 SumTask 인스턴스의 run() 메서드를 스택에 올리고 실행한다.
    • "thread-1" x001 인스턴스의 run() 메서드를 실행한다.
    • "thread-2" x002 인스턴스의 run() 메서드를 실행한다.
실행 결과 분석 - 메모리 구조 2
  • main 스레드는 두 스레드를 시작한 다음에 바로 task1.result , task2.result 를 통해 인스턴스에 있는 결과 값을 조회한다. 참고로 main 스레드가 실행한 start() 메서드는 스레드의 실행이 끝날 때까지 기다리지 않는다! 다른 스레드를 실행만 해두고, 자신의 다음 코드를 실행할 뿐이다!
  • "thread-1" , "thread-2"가 계산을 완료해서, result  연산 결과를 담을 때까지  2 정도의 시간이 린다. main 스레드는 계산이 끝나기 전에 result  결과를 조회한 것이다따라서 0 값이 출력된다.

실행 결과 분석 - 메모리 구조 3

 
  • 2초가 지난 이후에 "thread-1" , "thread-2"는 계산을 완료한다.
  • 이때 main 스레드는 이미 자신의 코드를 모두 실행하고 종료된 상태이다.
  • task1 인스턴스의 result 에는 "1275"가 담겨있고, task2 인스턴스의 result 에는 "3775"가 담겨있다.

여기서 문제의 핵심은 main 스레드가 "thread-1", "thread-2"의 계산이 끝날 때까지 기다려야 한다는 점이다. 그럼 어떻게 해야 main 스레드가 기다릴 있을까?

참고 - this의 비밀
어떤 메서드를 호출하는 것은, 정확히는 특정 스레드가 어떤 메서드를 호출하는 것이다.  스레드는 메서드의 호출을 관리하기 위해 메서드 단위로 스택 프레임을 만들고 해당 스택 프레임을 스택 위에 쌓아 올린다.
이때 인스턴스의 메서드를 호출하면, 어떤 인스턴스의 메서드를 호출했는지 기억하기 위해, 해당 인스턴스의 참조값을 스택 프레임 내부에 저장해 둔다.. 이것이 바로 우리가 자주 사용하던 this이다.

특정 메서드 안에서 this를 호출하면 바로 스택프레임 안에 있는 this 값을 불러서 사용하게 된다. 그림을 보면 스택 프레임 안에 있는 this this를 확인할 수 있다. 이렇게 this 가 있기 때문에 "thread-1" , "thread-2"는 자신의 인스턴스를 구분해서 사용할 수 있다. 예를 들어서 필드에 접근할 때 this를 생략하면 자동으로 this를 참고해서 필드에 접근한다.

정리하면 this는 호출된 인스턴스 메서드가 소속된 객체를 가리키는 참조이며이것이 스택 프레임 내부에 저장되어 있다.


Sleep 사용

Sleep의 활용한 해결
실행 결과
실행 결과 분석 - 시간의 흐름
  • "main" 스레드가 sleep(3000)을 사용해서 3초간 대기한다.
  • "thread-1" , "thread-2"는 계산에 2초 정도의 시간이 걸린다. 우리는 이 부분을 알고 있어서 "main" 스레드가 약 3초 후에 계산 결과를 조회하도록 했다. 따라서 계산된 결과를 받아서 출력할 수 있다.
  • 하지만 이렇게 sleep() 을 사용해서 무작정 기다리는 방법은 대기 시간에 손해도 보고, 또 "thread-1" , "thread-2"의 수행시간이 달라지는 경우에는 정확한 타이밍을 맞추기 어렵다.

나은 방법은 "thread-1" , "thread-2"가 계산을 끝내고 종료될 때까지 "main" 스레드가 기다리는 방법이다.

예를 들어서 "main" 스레드가 반복문을 사용해서 "thread-1" , "thread-2"의 상태가 "TERMINATED" 될 때까지 계 확인하는 방법이 있다.

while(thread.getState() != TERMINATED) { 
	//스레드의 상태가 종료될 때 까지 계속 반복
}
//계산 결과 출력

이런 방법은 번거롭고 또 계속되는 반복문은 CPU 연산을 사용한다. 이때 join() 메서드를 사용하면 깔끔하게 문제를 해결할 수 있다.

Join 사용

Join() 사용
실행 결과
실행 결과 분석 - 시간의 흐름

  • Join()은 InterruptedException을 던진다.
  • 실행 결과를 보면 5050이 계산된 것을 확인할 수 있다.

"main" 스레드에서 다음 코드를 실행하게 되면 "main" 스레드는 "thread-1" , "thread-2"가 종료될 때까지 기다린다. 이때 "main" 스레드는 "WAITING" 상태가 된다.

thread1.join();
thread2.join();
  • 예를 들어서 "thread-1" 이 아직 종료되지 않았다면 "main" 스레드는 thread1.join() 코드 안에서 더는 진행하지 않고 멈추어 기다린다. 이후에 "thread-1" 이 종료되면 "main" 스레드는 "RUNNABLE" 상태가 되고 다음 코드로 이동한다.  이때 "thread-2" 이 아직 종료되지 않았다면 "main" 스레드는 thread2.join() 코드 안에서 진행하지 않고 멈추어 기다린다. 이후에 "thread-2"이 종료되면 "main" 스레드는 "RUNNABLE" 상태가 되고 다음 코드로 이동한다. 이 경우 "thread-1" 이 종료되는 시점에 "thread-2"도 거의 같이 종료되기 때문에 thread2.join() 은 대기하지 않고 바로 빠져나온다.

Waiting (대기 상태)

  • 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.
  • join()을 호출하는 스레드는 대상 스레드가 TERMINATED 상태가 될 때까지 대기한다. 대상 스레드가 TERMINATED 상태가 되면 호출 스레드는 다시 RUNNABLE 상태가 되면서 다음 코드를 수행한다.

이렇듯 특정 스레드가 완료될 때 까지 기다려야 하는 상황이라면 join()을 사용하면 된다.

하지만 join()의 단점은 다른 스레드가 완료될 때까지 무기한 기다리는 단점이 있다. 비유를 하자면 맛집에 한번 줄을 서면 중간에 포기하지 못하고 자리가 때 까지 무기한 기다려야 한다. 만약 다른 스레드의 작업을 일정 시간 동안만 기다리고 싶다면 어떻게 해야 할까?

 

특정 시간만큼만 대기

join() 은 두 가지 메서드가 있다.

  • join() : 호출 스레드는 대상 스레드가 완료될 때까지 무한정 대기한다.
  • join(ms) : 호출 스레드는 특정 시간만큼만 대기한다. 호출 스레드는 지정한 시간이 지나면 다시 "RUNNABLE" 상태가 되면서 다음 코드를 수행한다.

Join(MS) 만큼 대기 코드
실행 결과
실행 결과 분석 - 시간의 흐름

  • 별도의 스레드에서 1 ~ 50까지 더하고, 그 결과를 조회한다.
  • join(1000) 을 사용해서 1초만 대기한다.
  • "main" 스레드는 join(1000)을 사용해서 "thread-1" 을 1초간 기다린다.
    • 이때 "main" 스레드의 상태는 "WAITING" 이 아니라 "TIMED_WAITING"이 된다.
    • 보통 무기한 대기하면 "WAITING" 상태가 되고, 특정 시간 만큼만 대기하는 경우 "TIMED_WAITING" 상태가 된다.
  • "thread-1" 의 작업에는 2초가 걸린다.
  • 1초가 지나도 "thread-1" 의 작업이 완료되지 않으므로, "main" 스레드는 대기를 중단한다. 그리고 "main "스레드는 다시 "RUNNABLE" 상태로 바뀌면서 다음 코드를 수행한다.
    • 이때 "thread-1" 의 작업이 아직 완료되지 않았기 때문에 task1.result = 0 이 출력된다.
  • "main" 스레드가 종료된 이후에 "thread-1"이 계산을 끝낸다. 따라서 작업 완료 result = 1275 이 출력된다.

 

728x90