Spring Async 사용시 ThreadPool

JopopScript·2023년 3월 21일
0

Spring Async

  • @Async 어노테이션을 붙이면 현재 실행중인 쓰레드가 아닌 다른쓰레드에서 비동기적으로 실행된다.
  • 쓰레드풀에 대한 정의가 없으면 SimpleAsyncTaskExecutor를 쓰는 줄 알았지만 사실은 ThreadPoolTaskExecutor를 쓴다
    • SimpleAsyncTaskExecutor는 작업마다 쓰레드가 생성되고 생성된 쓰레드는 회수되지않고, 캐싱도 되지 않는다.
  • 위와같은 상황을 막기위해서는 ThreadPoolTaskExecutor를 bean으로 등록하여 쓰레드가 생성되고 관리되도록 하자

사용한 툴

  • Visual VM
  • 사용목적: 단순히 어떤쓰레드가 생성되고 종료되었는지 확인용도

ThreadPoolTaskExecutor를 등록

테스트2 O 성공

  • Async요청 15개
  • setCorePoolSize: 2 / setQueueCapacity: 5 / setMaxPoolSize: 10
  • 예상한 결과: 첫요청시 2개쓰레드 생성 -> 7개요청까지 2쓰레드+5큐 -> 12요청 10쓰레드+5큐
    정말로 maxPool보다 queueCapacity가 먼저 적용될것인가? 두둥탁
  • 결과: keroro1 ~ keroro10 까지 10개 쓰레드가 생성됨

테스트3 X 실패

  • Async요청 1개
  • setCorePoolSize: 2 / setQueueCapacity: 5 / setMaxPoolSize: 10
  • 예상한 결과: 첫요청시 2개쓰레드 생성
  • 결과: keroro1 1개 쓰레드가 생성됨
  • 첫요청시 CorePoolSize만큼 늘어나는게 아니라 요청수까지만 늘어난다

테스트4 O 성공

  • Async요청 2개, 3개, 7개
  • setCorePoolSize: 2 / setQueueCapacity: 5 / setMaxPoolSize: 10
  • 예상한 결과: 2개쓰레드 생성
  • 결과: keroro1, keroro2 2개 쓰레드가 생성됨

테스트5 O 성공

  • Async요청 8개, 15개
  • setCorePoolSize: 2 / setQueueCapacity: 5 / setMaxPoolSize: 10
  • 예상한 결과: 10개쓰레드 생성
  • 결과: keroro1 ~ keroro10 10개 쓰레드 생성됨

테스트6 X 실패

  • Async요청 16개
  • setCorePoolSize: 2 / setQueueCapacity: 5 / setMaxPoolSize: 10
  • 예상한 결과: 10개쓰레드 생성
  • 결과: 에러로 종료, max + queue를 넘는 요청이들어오면 죽어버린다.
Caused by: org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@6cd15072[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$660/0x00000008004dd040@3c0e00a8

테스트7 번외

  • Async요청 15개
  • setCorePoolSize: 2 / setQueueCapacity: 5 / setMaxPoolSize: 10 / setKeepAliveSeconds: 3
  • 예상한 결과: 10개쓰레드 생성 -> 3초후 2개 쓰레드 생존
  • 결과: keroro1 ~ keroro10 10개 생성 -> keroro2, keroro9 생존

그외 테스트

  • 요청하는쪽에도 Thread.sleep으로 요청이 천천히 들어가게함
  • 10개까지 쓰레드가 안늘어남!

ThreadPool을 bean등록 없이

테스트1 X 실패

  • 별도로 bean등록 안하고 Async요청 15개
  • 내가 예상한 결과: 무지성으로 쓰레드 15개 생성되고 관리안되면서 쓰레드가 15개남아있어야함
  • 결과: task1 ~ task8까지 8개 쓰레드가 생성되어 8, 7로 두번나눠져서 실행됨
  • 왜 15개가 아니라 8개인가?
    i5-7500(4코어 8하이퍼쓰레드)를 사용중이여서 더이상 못늘어난다?
  • SimpleAsyncTaskExecutor을 쓰지 않는것 같다
    • SimpleAsyncTaskExecutor로 실행되었다면 쓰레드 명이 SimpleAsyncTask여야하는건 아닌가?
    • SimpleAsyncTaskExecutor는 쓰레드 재활용을 안하는데 8, 7 두번에 걸쳐서 한번 재활용을 했다?

그럼 어떤 ThreadPool을 사용하는가?

  • 등록된 bean중에서 executor 비슷한걸 찾아보자
  • applicationTaskExecutor 이놈인것 같다
  • applicationTaskExecutor <- taskExecutorBuilder
System.out.println(Arrays.toString(run.getBeanDefinitionNames()));
// ..., taskExecutorBuilder, applicationTaskExecutor, ...
  • ThreadPoolTaskExecutor의 maxPoolSize가 무제한이다... 이러면 안되는데 8이여야 하는데
public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
		implements AsyncListenableTaskExecutor, SchedulingTaskExecutor {

	private final Object poolSizeMonitor = new Object();

	private int corePoolSize = 1;

	private int maxPoolSize = Integer.MAX_VALUE;
  • Pool에서 coreSize가 8이긴 한데
    결과에 끼워 맞추기 인거 같지만... 왜냐하면! Thread 이름도 "task-"였기 때문이지...
@ConfigurationProperties("spring.task.execution")
public class TaskExecutionProperties {

	private final Pool pool = new Pool();

	private final Shutdown shutdown = new Shutdown();

	/**
	 * Prefix to use for the names of newly created threads.
	 */
	private String threadNamePrefix = "task-";

	public Pool getPool() {
		return this.pool;
	}

	public Shutdown getShutdown() {
		return this.shutdown;
	}

	public String getThreadNamePrefix() {
		return this.threadNamePrefix;
	}

	public void setThreadNamePrefix(String threadNamePrefix) {
		this.threadNamePrefix = threadNamePrefix;
	}

	public static class Pool {

		/**
		 * Queue capacity. An unbounded capacity does not increase the pool and therefore
		 * ignores the "max-size" property.
		 */
		private int queueCapacity = Integer.MAX_VALUE;

		/**
		 * Core number of threads.
		 */
		private int coreSize = 8;

		/**
		 * Maximum allowed number of threads. If tasks are filling up the queue, the pool
		 * can expand up to that size to accommodate the load. Ignored if the queue is
		 * unbounded.
		 */
		private int maxSize = Integer.MAX_VALUE;
  • bean중에서 Excecutor를 꺼내보자
  • 드디어 ThreadPoolTaskExecutor 인게 확실해 졌다!!!
  • 그러나 아직 의문인점 max가 무한인데 왜 8개까지만 만들었을까? -> queueCapacity가 거의 무한이여서 쓰레두가 늘어나비않고 큐에만 쌓였다
	public static void main(String[] args) {
		ConfigurableApplicationContext run = SpringApplication.run(DemoApplication.class, args);
		ThreadPoolTaskExecutor threadPoolTaskExecutor = run.getBean(ThreadPoolTaskExecutor.class);
		System.out.println("threadPoolTaskExecutor = " + threadPoolTaskExecutor);
		System.out.println("threadPoolTaskExecutor.getThreadNamePrefix()= " + threadPoolTaskExecutor.getThreadNamePrefix());
		System.out.println("threadPoolTaskExecutor.getPoolSize() = " + threadPoolTaskExecutor.getPoolSize());
		System.out.println("threadPoolTaskExecutor.getCorePoolSize() = " + threadPoolTaskExecutor.getCorePoolSize());
		System.out.println("threadPoolTaskExecutor.getMaxPoolSize() = " + threadPoolTaskExecutor.getMaxPoolSize());
		System.out.println("threadPoolTaskExecutor.getQueueSize() = " + threadPoolTaskExecutor.getQueueSize());
		System.out.println("threadPoolTaskExecutor.getQueueCapacity() = " + threadPoolTaskExecutor.getQueueCapacity());
	}

// threadPoolTaskExecutor = org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor@659f226a
// threadPoolTaskExecutor.getThreadNamePrefix()= task-
// threadPoolTaskExecutor.getPoolSize() = 3
// threadPoolTaskExecutor.getCorePoolSize() = 8
// threadPoolTaskExecutor.getMaxPoolSize() = 2147483647
// threadPoolTaskExecutor.getQueueSize() = 0
// threadPoolTaskExecutor.getQueueCapacity() = 2147483647

테스트에 사용한 코드

package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.concurrent.ListenableFuture;

import java.util.concurrent.ThreadPoolExecutor;

@SpringBootApplication
@EnableAsync
@Slf4j
public class DemoApplication {
	@Autowired KeroService keroService;

	@Bean
	ApplicationRunner run() {
		return args -> {
			log.info("run() start");
			for(int i = 0; i < 15; i++) {
// 				Thread.sleep(2000);
				ListenableFuture<String> f = keroService.kero(i);
				f.addCallback(System.out::println, error -> System.out.println(error.getMessage()));
			}
			log.info("run() exit");
			Thread.sleep(500000);
		};
	}

	@Component
	public static class KeroService {
//		SimpleAsyncTaskExecutor -> 1000개 동시요청이 들어오면 thread 1000개를 만들고 캐시/종료 하지않고 남아있음
		@Async
		public ListenableFuture<String> kero(int i) throws InterruptedException {
			log.info("Async start");
			Thread.sleep(2000);
			log.info("Async exit");
			return new AsyncResult<>("zero" + i);
		}
	}
//
//	@Bean
//	ThreadPoolTaskExecutor threadPool() {
//		ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
//		threadPool.setCorePoolSize(2); // 시작시0개 필요한시점 초기에 2개 생성
//		threadPool.setQueueCapacity(5); // 7개 요청이 동시에 들어오면 5개는 큐에쌓인다
//		threadPool.setMaxPoolSize(10); // 15개 요청이 들어오면 thread가 max까지(10) 늘어난다
//		threadPool.setKeepAliveSeconds(3); // 몇초동안 안쓸때 쓰레드를 종료시킬지
//		//max보다 queue가 먼저 적용된다.
//		threadPool.setThreadNamePrefix("keroro");
//		threadPool.initialize();
//		return threadPool;
//	}

	public static void main(String[] args) {
		try (ConfigurableApplicationContext run = SpringApplication.run(DemoApplication.class, args)) {}
	}
}
profile
tutorialMaster

0개의 댓글