Study/Mastering-Spring-5-Study

ch05. 스프링 프레임워크 심화 - 병렬 프로그래밍

hongeeii 2022. 12. 31.
728x90
반응형

Sync, Async vs Blocking, Non-Blocking

Sync, Async

Sync 방식

 메서드는 결과가 완성될 때까지 반환을 하지 않는다.

Async방식

 결과값이 결정되기 전에 일단 반환을 한다.
원하는 return 값을 찾기 전에 일단 빈값을 넘긴다.
클라이언트에서는 최종 결과를 받기전에 메서드로 부터 임시로 반환을 받는다.
그렇다면 클라이언트는 최종결과값을 어떻게 알 수 있을까?

Blocking, Non-Blocking

Sync, Asnyc는 메서드를 제공하는 곳에서의 입장에 대한 것이라면, Blocking, Non-Blocking은 메서드를
호출(사용)하는 곳, 즉 클라이언트에서의 입장에 대한 것이다.

 Async메서드는 결과를 완성하기 전에 일단 반환을 한다.
클라이언트는 다른 작업을 수행할 수 있다.
하지만 return값이 필요한 시점에서 getPriceAsync의 결과를 알고 싶을 때에는 다시 데이터를 조회해야한다.
클라이언트 입장에서는 항상 논블록킹이 아닌상태이다. (데이터 조회시 Blocking)

콜백 함수를 구현

논-블록킹 환경으로 개선하기 위해서는 콜백 함수를 구현해야 한다.
메서드를 제공하는 곳에서 결과가 완성되면 클라이언트로 결과가 나왔다고 알려주는 방법이다.

Thread Pool

 ThreadPool을 사용하는 이유

  • 프로그램 성능 저하를 방지하기 위해
    • 매번 발생되는 작업을 병렬처리하기 위해 스레드를 생성/수거하는데 따른 부담은 프로그램 전체적인 퍼포먼스 저하시킨다.
  • 다수의 사용자 요청을 처리하기 위해
    • 다수의 사용자의 요청을 수용하고, 빠르게 처리하고 대응하기 위해 스레드풀을 사용한다.

순차 프로그래밍 샘플

private static void task() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
  
  IntStream.range(0, 100).forEach(n -> {
    String threadName = Thread.currentThread().getName();
    System.out.println(threadName + ":task start" + n);
    task();
    System.out.println(threadName + ":task completed" + n);

해당 메서드를 100번 실행하면 단 하나의 쓰레드(main 쓰레드)로 모든 작업을 순차적으로 처리하기 때문에 100초의 시간이 걸릴 것이다.
짧은 시간에 모든 작업이 완료될 수 있도록 '병렬 프로그래밍'으로 개선한다.
Executors.newFixedThreadPool 메서드를 사용해 쓰레드풀을 정의한다.

private static final int THREAD_POOL_SIZE = 100;
	 
	private static final Executor EXECUTOR =
			Executors.newFixedThreadPool(THREAD_POOL_SIZE);

ThreadPool 종류 

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

ThreadPoolExcutor을 사용해서 커스텀 쓰레드풀도 만들 수 있다.

Executor을 활용해서 병렬로 동시에 실행하는 로직이다.

IntStream.range(0,100).forEach(n-> EXECUTOR.execute(()->{ // Runnable 구현
		    String threadName = Thread.currentThread().getName();
		    System.out.println(threadName + ":task start" + n);
		    task();
		    System.out.println(threadName + ":task completed" + n);
	});

ExecutorService(쓰레드 풀)의 작업 메서드

ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말한다.

  • execute()
    • 리턴값이 없는 Runnable객체를 작업큐에 저장한다.
    • 작업처리 결과를 받지 못한다.
    • 작업 처리 동중에 예외가 발생하면 스레드가 종료되고 해당 스레드를 스레드풀에서 제거한 뒤, 다른 작업처리를 위해서 새로운 스레드를 생성한다.
  • submit()
    • 작업처리결과를 받을 수 있도록 Future를 리턴한다.
    • 작업처리 도중에 예외가 발생하더라도 쓰레드는 종료되지 않고 다음 작업을 위해 재사용된다.
    • 쓰레드 생성 오버헤더를 줄이기 위해서 submit()을 사용하는 것이 좋다.

Future

Future를 이용하면 예약된 작업에 대한 결과를 알 수 있다.
executor.submit()은 Future객체를 리턴한다.
모든 작업을 예약할 때, Future를 따로 저장을 future.get()로 작업이 종료될 때 까지 기다린다.

주의할 점은 Future의 get() 메소드는 스레드가 작업이 완료될때까지 블로킹되므로 다른 코드를 실행할 수 없다.
★ get() 메소드를 호출하는 스레드는 새로운 스레드이거나 스레드풀의 또 다른 스레드가 되어야 한다.

@Async

@SpringBootApplication에 @EnableAsync를 먼저 선언해야한다.
반드시 public 메소드에 @Async 를 적용해야 한다.
스프링의 @Async는 AOP에 의해 동작하는데, @Async 어노테이션이 선언된 메서드는 비동기 메서드로 동작하게 된다.
비동기로 실행될 때 쓰레드는 새로 생성되는데, 별도의 설정을 하지 않는다면, 스프링 부트에서 만들어서 제공되는 ThreadPoolTaskExecutor에 의해 동작.
스프링 프레임워크의 AOP 내부 로직을 찾아가보면 최종적으로 아래의 클래스, 메서드에 도착하게 된다.

public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware {
...
protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
		if (CompletableFuture.class.isAssignableFrom(returnType)) {
			return CompletableFuture.supplyAsync(() -> {
				try {
					return task.call();
				}
				catch (Throwable ex) {
					throw new CompletionException(ex);
				}
			}, executor);
		}
		else if (ListenableFuture.class.isAssignableFrom(returnType)) {
			return ((AsyncListenableTaskExecutor) executor).submitListenable(task);
		}
		else if (Future.class.isAssignableFrom(returnType)) {
			return executor.submit(task);
		}
		else {
			executor.submit(task);
			return null;
		}
	}

@Async로 선언된 메서드는 리턴타입에 따라서 내부적으로 상이하게 동작한다.

  • CompletableFuture
    • 완벽하게 non-blocking 하게 동작.
  • ListenableFuture
  • Future
    • 메서드의 결과를 전달받아야 할 때
    • future의 get메서드는 메서드의 결과를 조회할 때까지 계속 기다림
    • 메서드의 수행이 완료될 때까지 기다려야함으로 블록킹현상이 발생.
    • 논블록킹으로 동작하게 하고 싶다면 콜백 메서드를 처리 하는 로직을 구현
  • 리턴타입이 없는 경우
    • 비동기로 처리 해야하는 메서드가 처리 결과를 전달할 필요가 없는 경우.
    • @Async 어노테이션의 리턴 void를 선언해주면 됨.
    • 처리 결과가 완료될 때까지 계속해서 기다릴 필요가 전혀 없음(return null)으로, non-blocking 수행

example

 

 

 

728x90
반응형

추천 글