본문 바로가기
프로그래밍/Java

[Java] Thread Pool(스레드 풀)

by Lim-Ky 2017. 7. 7.
반응형

Thread Pool(스레드 풀) 이란??


오늘은 스레드 풀에대해서 간략하게 알아보자.


간단하다. 스레드를 미리 만들어 놓은 하나의 풀장...이라고 생각하면된다.

군대를 빗대어보면, 전쟁이 나서 사방팔방에서 국지전을 펼친다고 생각해보자.

그때그때 추가병력을 요청할때마다 당신이 지휘관이라면, 1명씩 지원을 보낼텐가???


아니다. 미리 100명의 군인을 섭외해서 다중적으로 발생되는 국지전을 대비해 예비 병력을 갖추고 즉각 국지전에 대응해야한다.


이제 감이 좀 잡혔을 것이라고 본다.


그렇다면 이제 SW적으로 접근해보자.

"스레드"라는 녀석이 생성될 때 컴퓨터 내부적으로 운영체제(OS)가 요청을 받아들여 메모리공간을 확보해주고 

그 메모리를 스레드에게 할당해준다. 스레드는 동일한 메모리영역에서 생성되고 관리되지만, 생성/수거에 드는 비용을 무시할 수 없다.


그렇기 때문에 요청이 들어올 때 마다 스레드를 생성하고 일을 다하면 수거하고 하는 작업은 프로그램 퍼포먼스에 지대한 영향을 줄 수 있다.

따라서 스레드를 미리 만들어 놓는 것이다.



Thread Pool(스레드 풀)의 동작 원리


아래 첨부한 그림을 통해서 확실히 개념을 정립하자!

우리가 만든 어플리케이션에서 사용자로부터 들어온 요청을 작업큐에 넣고 

스레드풀은 작업큐에 들어온 Task일감을 미리 생성해놓은 Tread들에게 일감을 할당한다.

일을 다 처리한 Thread들은 다시 어플리케이션에게 결과값을 리턴한다.




자바에서는 스레드풀을 생성하고 사용할 수 있도록 java.util.concurrent Package에서 ExecutorService 인터페이스Executors 클래스를 제공하고 있다. Executors의 다양한 정적 메서드를 통해 ExecutorService 구현객체를 만들어서 사용할 수 있으며, 그것이 바로 스레드 풀이다. 






Thread Pool(스레드 풀) 을 왜 사용해야해???


이미 답은 나왔지만, 크게 2가지만 기억하면 된다.


1. 프로그램 성능저하를 방지하기 위해


매번 발생되는 작업을 병렬처리하기 위해 스레드를 생성/수거하는데 따른 부담은 프로그램 전체적인 퍼포먼스 저하시킨다.

따라서 스레드풀을 만들어 놓는다.


2. 다수의 사용자 요청을 처리하기 위해


서비스적인 측면으로 바라볼 때

특히 대규모 프로젝트에서 중요하다. 다수의 사용자의 요청을 수용하고, 빠르게 처리하고 대응하기 위해 스레드풀을 사용한다.




Thread Pool(스레드 풀)의 단점???


1. 과유불급...너무 많이 만들어 놓았다가 메모리 낭비만 발생.


많은 병렬처리를 예상해서 1억개의 스레드를 만들어 놓았다고 생각해보자. 실제로 100개정도의 요청과 병렬처리를 했다. 그렇다면.. 나머지 스레드들은 아무일도 하지않고 메모리만 차지하는 최악의 경우가 발생될 수 있다.



2. 노는 스레드가 발생될 수 있다.


1번과 비슷하지만 조금 다르다. 

예를 들어 A,B,C 스레드 3개가 있는데, 병렬적으로 일을 처리하는 과정에서 A,B,C 작업완료 소요시간이 다른 경우 스레드 유휴시간 즉

A스레드는 아직 일이 많아서 허덕이고 있는데, B,C는 일을 다하고 A가 열심히 일하는 것을 보고 놀고만 있는 유휴시간이 발생된다.

자바에서는 이를 방지하기 위해 forkJoinPool 을 지원한다. 아래 링크를 통해 알아보자


http://hamait.tistory.com/612




Thread Pool(스레드 풀) 생성/종료


1. 스레드 풀 생성


ExecutorService 구현 객체는 Executors 클래스의 다음 두가지 메소드 중 하나를 이용해 간편하게 생성할 수 있다.

생성방밥에 앞서 알아야 할 개념이 있다.


초기 스레드 수 : ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드 수

코어 스레드 수 : 스레드가 증가한 후 사용되지 않은 스레드를 스레드 풀에서 제거할 때 최소한으로 유지해야할 수

최대 스레드 수 : 스레드풀에서 관리하는 최대 스레드 수



1, newCachedThreadPool()

초기스레드 수, 코어스레드 수 0개 최대 스레드 수는 integer 데이터타입이 가질 수 있는 최대 값(Integer.MAX_VALUE)

스레드 개수보다 작업 개수가 많으면 새로운 스레드를 생성하여 작업을 처리한다.

만약 일 없이 60초동안 아무일을 하지않으면 스레드를 종료시키고 스레드풀에서 제거한다.



2. newFixedThreadPool(int nThreads)

초기 스레드 개수는 0개 ,코어 스레드 수와 최대 스레드 수는 매개변수 nThreads 값으로 지정,

이 스레드 풀은 스레드 개수보다 작업 개수가 많으면 마찬가지로 스레드를 새로 생성하여 작업을 처리한다.

만약 일 없이 놀고 있어도 스레드를 제거하지 않고 내비둔다.  


newCachedThreadPool(),newFixedThreadPool() 메서드를 사용하지 않고 직접 스레드 개수들을 설정하고 싶다면

직접 ThreadPoolExecutor 객체를 생성하면 된다. 




2. 스레드 풀 종료


스레드 풀에 속한 스레드는 기본적으로 데몬스레드(주 스레드를 서포트하기 위해 만들어진 스레드, 주 스레드 종료시 강제 종료)가 아니기 때문에 main 스레드가 종료되어도 작업을 처리하기 위해 계속 실행 상태로 남아있다. 즉 main() 메서드가 실행이 끝나도 어플리케이션 프로세스는 종료되지 않는다. 어플리케이션 프로세스를 종료하기 위해선 스레드 풀을 강제로 종료시켜 스레드를 해체시켜줘야 한다. 

ExecutorService 구현객체에서는 기본적으로 3개 종료 메서드를 제공한다.


excutorService.shutdown();

 - 작업큐에 남아있는 작업까지 모두 마무리 후 종료 (오버헤드를 줄이기 위해 일반적으로 많이 사용.)


excutorService.shoutdownNow();

 - 작업큐 작업 잔량 상관없이 강제 종료


excutorService.awaitTermination(long timeout, TimeUnit unit);

 - 모든 작업 처리를 timeout 시간안에 처리하면 true 리턴 ,처리하지 못하면 작업스레드들을 interrupt()시키고 false리턴




Thread Pool(스레드 풀)에게 작업시키기


스레드 풀에게 작업을 시키기 전... 작업을 생성시켜야 작업처리를 요청할 수 있다.

작업 생성은 Runnable 인터페이스 or Callable 인터페이스를 구현한 클래스로 작업요청할 코드를 삽입해 작업을 만들 수 있다.

둘의 차이점은 Runnable의 run() 메서드는 리턴값이 없고, Callable의 call() 메서드는 리턴 값이 있다. 

자세한건 doc을 참고하시길!


이제 마지막!! 다왔다... 스레드풀에게 작업을 처리 요청을 하기 위해선 execute(), submit() 2가지 메서드가 있다.


execute();

 - 작업 처리 결과를 반환하지 않는다.

 - 작업 처리 도중 예외가 발생하면 스레드가 종료되고 해당 스레드는 스레드 풀에서 제거된다. 

 - 다른 작업을 처리하기 위해 새로운 스레드를 생성한다.


submit();

 - 작업 처리 결과를 반환한다.

 - 작업 처리 도중 예외가 발생하더라도 스레드는 종료되지 않고 다음 작업을 위해 재사용

 - 스레드의 생성 오버헤드를 방지하기 위해서라도 submit() 을 가급적 사용한다.




마무리는 예제로... 앞에서 한 내용이 모두 들어가있다. 주석을 참고하시길...


package ThreadPoolExample;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class main {

	public static void main(String[] args) {
		// ExecutorService 인터페이스 구현객체 Executors 정적메서드를 통해 최대 스레드 개수가 2인 스레드 풀 생성 
		ExecutorService executorService = Executors.newFixedThreadPool(2);
		
		for(int i = 0; i < 10; i++){
			Runnable runnable = new Runnable() {
				
				@Override
				public void run() {
					//스레드에게 시킬 작업 내용
					ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
					
					int poolSize = threadPoolExecutor.getPoolSize();//스레드 풀 사이즈 얻기
					String threadName = Thread.currentThread().getName();//스레드 풀에 있는 해당 스레드 이름 얻기
					
					System.out.println("[총 스레드 개수:" + poolSize + "] 작업 스레드 이름: "+threadName);
					
					//일부로 예외 발생 시킴
					int value = Integer.parseInt("예외");
				}
			};
	
			//스레드풀에게 작업 처리 요청
			executorService.execute(runnable);
			//executorService.submit(runnable);
			
			
			//콘솔 출력 시간을 주기 위해 메인스레드 0.01초 sleep을 걸어둠.
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} 
			
			
		}
		
		//스레드풀 종료
		executorService.shutdown();
	
		
		
		
	}

}


출력 결과





스레드 풀을 기본적으로 만들면 pool-n형식의 이름으로 스레드 풀 이름이 부여된다. 

0번째는 기본돌아가고있는 스레드풀인가???(공부해봐야함..)일단 우리는 pool-1을 만들었고,

execute()메서드를 실행시켰기 때문에 스레드 작업 중 예외가 나면 스레드를 바로 종료시키고 새로운 스레드를 생성한다. 따라서 결과가 thread-1,2,3,4,5. ... 10 까지 늘어났을 것이다.


만약 submit()메서드를 실행시키면, 아래와 같이 예외가 발생되도 해당 스레드를 죽이지 않고 계속 재사용한다.






스레드 풀에 대한 더 많은 이야기가 있지만, 다음을 기약하며... . 


끝으로 인상깊은 글을 스크랩한다.


멀티쓰레드 처리율(throughput)   임백준님의 Akka 시작하기에서 발췌 
아카를 이용한 리팩토링을 끝마쳤을 때, 똑같은 컴퓨터 위에서 전과 동일한 몬테 카를로 시나리오를 수행하는데 걸리는 시간이 6시간에서 2시간으로 단축되었다. 66%의 시간이 절약된 것이다. 결과를 확인한 사람들은 깜짝 놀랐다. 단순히 자바 스레드에서 아카로 라이브러리를 바꾸었을 뿐인데 그렇게 엄청난 차이가 있을 수 있냐며 고개를 갸웃거렸다. 물론 이런 차이를 일반화할 수는 없다. 이런 결과 하나를 가지고 아카가 자바 스레 드보다 3배 빠르다고 말하는 어리석은 사람은 없을 것이다. 아카도 내부적으로 자 1 아카에 대하여 - 017 바 스레드를 사용하기 때문에 그런 비교 자체가 성립하지 않는다. 하지만 일반적 인 차원에서 짚고 넘어갈만한 부분도 있다. 이렇게 커다란 차이가 어디에서 비롯 되었는지 이해하려면 우선 암달의 법칙Amdahl’s law을 생각해볼 필요가 있다. 암달의 법칙은 이렇다. “멀티코어를 사용하는 프로그램의 속도는 프로그램 내부에 존재하는 순차적sequential 부분이 사용하는 시간에 의해서 제한된다.” Thread나 Task를 만들어서 ExecutorService에게 제출하는 식으로 동시성 코드를 작성하면 여러 개의 스레드가 동시에 작업을 수행한다. 하지만 프로그램 안에는 Thread나 Task가 포함하지 않는 코드가 존재한다. 여러 개의 스레드가 동시에 작업을 수행하더라도 synchronized 블록이나 데이터베이스, 네트워크 API 호출 등을 만날 때 다른 스레드와 나란히 줄을 서서 순차적으로 작업을 수행 해야 하는 경우도 있다. 암달의 법칙은 프로그램이 낼 수 있는 속도의 상한이 이런 순차적 코드가 사용하는 시간에 의해서 제한된다고 말하는 것이다. 이러한 순차적 코드의 또 다른 이름은 블로킹blocking 콜이다. 문제는 스레드 자체 가 아니라 스레드를 사용하면서 자기도 모르게 만들어내는 블로킹 콜이다. 조금 과장해서 말하자면 자바 개발자가 스레드를 이용해서 만들어내는 ‘동시성’ 코드는 일종의 신기루다. 사실은 코드 곳곳에 존재하는 블로킹 콜, 순차적 코드 때문에 전 체적인 프로그램의 처리율은 이미 상한이 정해져 있지만 여러 개의 스레드가 ‘동 시에’ 동작한다는 사실로부터 위안을 받을 뿐이다.

출처: http://hamait.tistory.com/612 [HAMA 블로그]








반응형

댓글11