3주차 ( 쓰레드 )

2019. 7. 13. 14:18카테고리 없음


      [ ※ 실습예제로 쓰인 소스는 자바의 정석 개정판을 참고하였습니다. ]

      [ ※ 참고할만한 아주 좋은 무료강의 사이트 : https://programmers.co.kr/learn/courses/9/lessons/270 ]

  • Thread 란?

- 어떠한 프로그래밍 언어로 소스를 짜서 만든 것을  '프로그램' 이라고 하며, 프로그램을 동작하도록 하면 '프로세스'가 실행 된다.
  여기서 프로세스는 어떠한 일을 수행함에 있어서 프로그래머가 정의한 일련의 동작들을 말하는데 각 동작들을 직접적으로 처리하는 것이 쓰레드 이다.
- 실생활에서의 예를 들면 공장의 한 라인에 노동자들이 물건을 올려놓는 행위에서 찾을 수 있는데 여기서 공장은 '프로그램', 라인은 '프로세스', 노동자는 '쓰레드'가 될 수 있다.
*Thread 의 구현과 실행

public class ThreadEx0 extends Thread{
	@Override
	public void run(){
		System.out.println("running to First-Thread!");
	}
	
	public static void main(String[] args) {
		ThreadEx0 thread0 = new ThreadEx0();
		thread0.start();
		//thread0.run();
	}
}

 

 

Q. run() 메서드를 오버라이드했는데, 왜 start()로 실행시켜줬을까?
     - run으로 직접 메서드를 실행해도 예제에서 하고자 하는 내용은 동일하게 실행됨 (예제 내용에 한해서는 동일)
     start가 run을 어떤 방식으로든 실행시키는건 확인됨, 그렇다면 어떤걸 어느 경우에 써야할까?

A. Thread를 이용하여 여러작업을 동시에 처리하기 위한 것
     - run() 의 경우, 초기에 실행된 main 쓰레드 안에서 위 소스 속 'thread0'이라는 객체의 메서드를 호출한 것이고 (1개의 콜스택, main콜스택에서 thread0의 run()메서드를 실행시킴)
       start() 의 경우, 새로운 쓰레드가 작업을 실행할 수 있도록 또다른 콜스택을 생성하여 run()을 호출한 것 (main콜스택과 별개로 새로운 콜스택을 생성)
     - 2개 이상의 콜스택을 가지면, JVM에서 내부적인 우선순위에 따라 왔다갔다 처리하면서 동시에 처리하는 것 처럼 보여짐

[결론]여러 작업을 동시에 처리하기 위해선 새로운 콜스택을 만들어 주는 start() 메서드를 통해 쓰레드를 실행시키고,
           여기서 추가된 쓰레드는 main 쓰레드와 동시에 처리되는 것이  아니라 JVM의 context Switching 을 통해 하나씩 번갈아가며 처리되는 것이다.

  •  

    Q. 이미 다른 클래스를 상속받고 있는 경우에는?

    A. Runnable 인터페이스를 이용하여 쓰레드를 생성한다.
        - Runnable 인터페이스는 run() 메서드만 정의되어있는 인터페이스로 해당 메서드를 구현 한 다음, 
          구현한 클래스의 인스턴스를 Thread 클래스의 생성자에 매개변수로 넘겨 사용할 수 있다. (위의 Thread 생성자 참고)
        - 다중상속이 불가함에 따라 Runnable 인터페이스 구현을 통해 쓰레드를 활용하는 것이 일반적임.

     

  • Thread 주의사항
    1.  한 번 종료된 쓰레드는 다시 실행할 수 없다. (= 하나의 쓰레드에 대해 start()를 두번 할 수 없다.)
    2. run() 메서드가 종료하면 해당 스레드는 종료된다. (= 스레드를 계속 실행시키고자 할 경우 run() 메서드를 무한 루프로 묶어준다.)
    3. 한 쓰레드에서 다른 쓰레드를 종료 할 수 있다.
    4. main 쓰레드가 종료되더라도 다른 쓰레드가 작업을 마치지 않은 상태라면 프로그램은 종료되지 않는다.

  • Thread 콜스택

     

    - 새로 생성한 쓰레드에서 고의로 에러를 발생시켜 예외가 발생한 시점의 콜스택을 출력
       throwException → run() (처리 할 내용이 없는 main쓰레드는 먼저 종료되고 새로 생성된 쓰레드 t1의 호출 스택이 출력되었다. = 주의사항4번)

     

    - run()으로 실행시켰기 때문에 따로 쓰레드를 생성하지 않았고, 위의 예제와 다르게 main 쓰레드 내에서 throwException → run()  → main이 발생한 것을 확인할 수 있다.

  • 쓰레드의 6가지 상태



  • 싱글쓰레드, 멀티쓰레드


    - 위 그래프 사진을 보면 하나의 쓰레드로 두 작업을 처리하는 것과 두 개의 쓰레드로 두 작업을 처리하는 것을 확인할 수 있다. 소스를짜보면서 확인해보자.

     

     

     


    - 컴퓨터 성능에 따라 싱글, 멀티쓰레드의 차이가 거의 없음을 확인할 수 있다.
    - 각 결과는 실행 할 때마다 출력 순서나 소요시간이 다른데 이는 JVM이 쓰레드 스케쥴을 어떻게 판단하고 설계하느냐에 따라 매번 달라지기때문에 일정할 수 없다. 
       이같이 각 쓰레드에게 순간마다 할당되는 시간이 일정하지 않은 불확실성을 염두하고 프로그램을 설계해야한다.

    Q. 어떻게 처리하는게 빠를까?

    A. 각 쓰레드가 서로 다른 자원을 사용하는 경우 멀티쓰레드로 처리하는 것이 빠르다. 
        예를 들어, 사용자로부터 데이터를 입력받거나 네트워크로 파일을 주고받는다던지 
        프린터로 파일을 출력하는 입출력의 경우 등 대기하는 시간 동안 다른 쓰레드를 처리 할 수 있기때문에 훨씬 효율적으로 활용할 수 있다.

    - 입력을 받고나서 숫자 출력 예제

     


    - 쓰레드를 사용하여 숫자 출력을 하는 중간에 입력받는 예제

     

    [결론] 입력을 받기 이전에 이미 카운트가 진행이 되어 입력을 기다렸다 진행하는 첫 번째 케이스보다는 멀테쓰레드로 처리한 두 번째 케이스가 훨씬 효율적이다.

  • 쓰레드의 우선순위
    - 앞서 소개된 Thread의 메소드들 중 쓰레드의 우선순위를 지정하거나 반환받을 수 있는 메서드가 구현되어있음을 확인할 수 있었다. 이를 적용하여 설정/출력 예제를 진행해보자
    - 우선순위 기본값은 5, 지정범위는 1~10, 높을수록 우선순위가 부여된다.

     

    - 실행 결과를 보면 알 수 있듯이 우선순위를 더 높게 주더라도 차이가 전혀 없었다.
    [검색결과]cpu의 코어 갯수, 쓰레드의 갯수, 처리되는 작업크기에 따라 크게 영향을 미치지 못 할 수 있다. 
                     우선순위에 따라 순차 적용하고자 하는 경우 PriorityQueue에 저장해놓고 높은 순위의 작업 먼저 처리할 수 있도록 하는 것이 대다수의 의견임.
                     [ 참고URL :  http://asuraiv.blogspot.com/2015/11/java-priorityqueue.html ]

  • 쓰레드 그룹
    - 서로 관련된 쓰레드끼리 그룹화하여 다루기 위한 것으로, 윈도우의 폴더형식처럼 상/하위 디렉터리로 묶어서 관리할 수 있다.
    - 명시하지 않으면 기본적으로 생성된 main 쓰레드 안에 속하게 된다.

     

    - 결과를 살펴보면 아래의 정보를 확인할 수 있다.
      1. 현재 쓰레드의 이름(main), 
      2. 현재 쓰레드 그룹에 포함되어 활성상태에 있는 쓰레드 그룹의 수(main쓰레드 내에 설정한 3개의 쓰레드), 
      3. 자신이 속한 그룹에 작업이 완료되지 않은 쓰레드의 수(main포함)
      4. main 그룹에 소속된 쓰레드 리스트

  • 데몬 쓰레드
    - 다른 일반 쓰레드의 작업을 돕는 보조적인 역할의 쓰레드 (보조하던 일반 쓰레드가 종료되면 함께 종료됨)
    - 데몬쓰레드는 가비지 컬렉터나 워드프로세서의 자동저장 등 메인 프로그램 곁에서 무한루프를 통해 대기하다가 특정한 조건이 만족되면 작업을 수행하고 다시 대기하는 특징을 가진다.
    - 일반 쓰레드와 동일하게 생성하나 setDemon(true); 를 추가로 호출해야한다.

     

    - 1~10까지 카운트 중간에 2초가 경과하면 파일을 자동 저장하는 예제
    [주의사항] setDemon 설정은 start() 이전에 설정해야 적용됨 (IllegalThreadStateException 발생)

  • 쓰레드의 실행제어
    - sleep() : 매개변수로 받은 값의 타입에 따라 그 시간동안 쓰레드를 잠들게 한다.
                    [테스트] 동작 시간이 똑같은 쓰레드 두 개(A, B)를 main에서 실행하고, A에 sleep(2000)을 준다음 결과를 보면
                                  예상 => main > B > A 순서로 종료
                                  결과 => A > B > main 순서로 종료
                                   => sleep() 함수는 항상 현재 실행중인 쓰레드에 대해 작동하기 때문에 main에 적용되어 가장 늦게 종료됨을 확인.
    - interrupt() : 진행 중인 쓰레드의 작업이 종료되기 전에 취소할 경우 사용하는데 취소 요청(인스턴스 변수 false로 변경)만 할 뿐이지 강제적으로 종료시키진 못함.

     

    isInterrupted() 에서 바뀐 인스턴스 변수를 가져와 interrupt 발생 여부(이 예제에서는 dialog 입력 유무)를 판단하여 쓰레드를 종료한다.
    - suspend(), resume() : suspend()는 sleep()처럼 멈추게 하지만 resume()을 호출해야만 다시 실행대기 상태가 된다.
    - stop() : 호출되는 즉시 쓰레드가 종료된다.
    - 위의 메서드들을 활용한 예제들을 여러개 테스트 해 본 결과 각 메서드로 일정한 결과값을 얻기 힘들었고, 이 때문에 현재 권장하지 않는 메서드로 deprecated 처리 되있는 상태이다.
      이해하기 쉽도록 현실세계에 비유하여 표현한 게시글을 소개한다. [ 참고URL : http://gorakgarak.tistory.com/452]

  • 쓰레드의 동기화
    - 싱글쓰레드의 경우, 프로세스 내에서 단 하나의 쓰레드만 가지고 작업하기 때문에 자원을 이용한 작업에 별 문제가 없지만
      멀티쓰레드의 경우, 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다. 이때 발생하는 문제에 대한 해결점으로 '임계영역', '잠금'을 활용한다.

    1. 우선 동기화가 왜 필요한지 간단한 예제로 확인해본다. 

     

    - 잔고가 음수인 경우가 생긴것을 확인할 수 있다. (한 쓰레드가 if문의 조건식을 통과해 돈을 출금하는 순간에 다른 쓰레드가 끼어들어 출금을 먼저 했기 때문에 발생하는 문제)

    2. 동기화 적용한 예제

     

    - withdraw에 synchronized 를 추가하여 간단하게 동기화를 적용할 수 있다. 한 쓰레드에 의해 withdraw() 가 호출되면 종료되어 lock이 반납 될 때까지 대기상태에 머물렀다가 뒤이어 수행된다.
    - Account 의 balance 변수를 private으로 변경되지않도록 설정하는 것이 중요하다.

  • wait(), notify()
    - 위의 경우처럼 하나의 쓰레드가 모두 처리되야 다음 쓰레드가 자원을 사용할 수 있다면 사용중인 쓰레드가 락을 가진 상태로 오랜시간 보내지 않도록 처리해야한다.
      이때 wait()로 락을 반납하고 다른 쓰레드가 락을 얻어 작업을 수행하도록 한다. 준비중에 있다가 나중에 진행할 수 있는 능력이 될때 notify()로 다시 락을 얻어 작업을 재 진행한다.
      단, 잠재우고 다시깨우는 이러한 함수는 현재 락을 소유한 쓰레드만이 수행할 수 있으며 락이 없는 쓰레드가 수행하는 경우 IlegalMonitorStateException이 발생한다.

     

     

  • 실습예제 1. 위의 내용들을 토대로 실생활에서 발생하는 상황을 쓰레드로 표현하여 간단하게 각자 발표해보자.
    [2018/11/24] 김윤권, 최윤진, 배준