4

I am trying to understand the behavior of @Async in Spring Boot, by using the default SimpleAsyncTaskExecutor (where I don't explicitly define any Executor bean). According to the documentation of SimpleAsyncTaskExecutor, 'By default, the number of concurrent threads is unlimited'. But on running the sample code below, all I can see is that only 8 threads are fired up, and the remaining tasks are waiting to get a new thread to execute them. I know this can be prevented with a custom Executor where I can define the size of the thread pool. But I want to know if my understanding of SimpleAsyncTaskExecutor is correct or not, or something is not right with my code.

Main class

@SpringBootApplication
@EnableAsync
public class MainRunner {

    private static final Logger LOGGER = LoggerFactory.getLogger(MainRunner.class);

    public static void main(String[] args) {
        ApplicationContext applicationContext = SpringApplication.run(MainRunner.class);
        MyService myService = (MyService) applicationContext.getBean("myService");
        LOGGER.info("Starting the submission of tasks...");
        for (int i = 1; i <= 50; i++) {
            myService.doSomething("Number" + i);
        }
        LOGGER.info("Finished submission of tasks...");

    }
}

MyService class

@Service
public class MyService {

    private static final Logger LOGGER = LoggerFactory.getLogger(MyService.class);

    @Async
    public void doSomething(String userName) {
        LOGGER.info(Thread.currentThread().getName() + ", "
                + Thread.currentThread().getId() + ", NAME: " + userName + " STARTING...");
        for (int i = 0; i < 10000; i++) {
            for (int j = 0; j < 1000000; j++) {
                int res = i + j;
            }
        }
        LOGGER.info(Thread.currentThread().getName() + ", "
                + Thread.currentThread().getId() + ", NAME: " + userName + " COMPLETE...");
    }
}

I expect to see that all 50 tasks are started, and they don't wait for a thread ready to process them. But the above code causes first 8 tasks submitted to start off, and the remaining tasks are waiting for the running tasks to complete in order to be picked up and executed.

2019-09-19 09:33:06.560  INFO 17376 --- [           main] sample.MainRunner                        : Starting the submission of tasks...
2019-09-19 09:33:06.564  INFO 17376 --- [           main] sample.MainRunner                        : Finished submission of tasks...
2019-09-19 09:33:06.566  INFO 17376 --- [         task-8] sample.MyService                         : task-8, 45, NAME: Number8 STARTING...
2019-09-19 09:33:06.566  INFO 17376 --- [         task-1] sample.MyService                         : task-1, 38, NAME: Number1 STARTING...
2019-09-19 09:33:06.566  INFO 17376 --- [         task-7] sample.MyService                         : task-7, 44, NAME: Number7 STARTING...
2019-09-19 09:33:06.567  INFO 17376 --- [         task-4] sample.MyService                         : task-4, 41, NAME: Number4 STARTING...
2019-09-19 09:33:06.566  INFO 17376 --- [         task-6] sample.MyService                         : task-6, 43, NAME: Number6 STARTING...
2019-09-19 09:33:06.567  INFO 17376 --- [         task-2] sample.MyService                         : task-2, 39, NAME: Number2 STARTING...
2019-09-19 09:33:06.567  INFO 17376 --- [         task-5] sample.MyService                         : task-5, 42, NAME: Number5 STARTING...
2019-09-19 09:33:06.567  INFO 17376 --- [         task-3] sample.MyService                         : task-3, 40, NAME: Number3 STARTING...

It waits for the first 8 to complete, and then the remaining tasks are executed. Is my understanding of SimpleAsyncTaskExecutor wrong here?

IceMan
  • 123
  • 2
  • 11
  • Probably queries your number of CPUs, creates a thread for every core. This is more efficient that having 50 threads and context switches. – Jazzwave06 Sep 19 '19 at 14:38
  • @sturcotte06 so the assumption of 'unlimited threads' is limited by the number of threads per core? – IceMan Sep 19 '19 at 14:51
  • I would not assume the thread pool's strategy for creating threads. Just assume it's been optimized. If you have issues (not using all of CPU resources or too many context switches), then you can switch implementation and see if you get better results. 50 threads is usually too much anyway, unless you're using blocking IO. – Jazzwave06 Sep 19 '19 at 15:02

3 Answers3

5

Your code is not using the SimpleAsyncTaskExecutor.

Using @EnableAsync simply

Enables Spring's asynchronous method execution capability, similar to functionality found in Spring's XML namespace.

Spring does not create a SimpleAsyncTaskExecutor based on that annotation. Looking at the log output:

2019-09-19 12:45:43.475 INFO 19660 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'

It seems that Spring is creating it appears a default ThreadPoolTaskExecutor, which is probably tied the number of cores on your machine (I didn't really check though).

If you really want the SimpleAsyncTaskExecutor you can implement the AsyncConfigurer interface in your configuration

@SpringBootApplication
@EnableAsync
public class MainRunner implements AsyncConfigurer {

  private static final Logger LOGGER = (Logger) LoggerFactory.getLogger(MainRunner.class);

  @Override
  public Executor getAsyncExecutor() {
      return new SimpleAsyncTaskExecutor();
  }

  public static void main(String[] args) {
    ApplicationContext applicationContext = SpringApplication.run(MainRunner.class);
    MyService myService = (MyService) applicationContext.getBean("myService");
    LOGGER.info("Starting the submission of tasks...");
    for (int i = 1; i <= 50; i++)
    {
      myService.doSomething("Number" + i);
    }
    LOGGER.info("Finished submission of tasks...");
  }
}
Jason Warner
  • 2,469
  • 1
  • 11
  • 15
3

When there is no custom Async TaskExecution configuration defined, Springboot will use the default one.
TaskExecutionAutoConfiguration with the bean name applicationTaskExecutor.

You can find the following log line during Springboot startup:

INFO  org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.initialize - Initializing ExecutorService 'applicationTaskExecutor'

During build this default TaskExecution configuration, Springboot will use TaskExecutionProperties which contains the default configs values.

Inside it, we can see the default used coreSize which is 8

private int coreSize = 8;

Of course, we can override the default Async TaskExecution configuration and/or create multiple configs.

@Configuration
@EnableAsync
public class EnableAsyncConfig implements AsyncConfigurer {

    @Bean
    public Executor taskExecutor() {
        // Async thread pool configuration
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(40);
        executor.initialize();
        return executor;
    }

}
Ahmed Nabil
  • 17,392
  • 11
  • 61
  • 88
1

Yes threads are limited by available cores in CPU here

Core

A core is usually the basic computation unit of the CPU - it can run a single program context (or multiple ones if it supports hardware threads such as hyperthreading on Intel CPUs)

CPU

A CPU may have one or more cores to perform tasks at a given time. These tasks are usually software processes and threads that the OS schedules. Note that the OS may have many threads to run, but the CPU can only run X such tasks at a given time, where X = number cores * number of hardware threads per core. The rest would have to wait for the OS to schedule them whether by preempting currently running tasks or any other means.

What will happen if you have more number of threads ?

Suppose if you have X number of threads then CPU scheduler is giving each one of those X threads some share of CPU time. Some threads will be running in parallel (if you have 4 cores, then 4 threads will be running in parallel at any one time or if you have 4 core hyperthreading on Intel CPUs then total 8 threads will be running parallel) and remaining threads will be in waiting or running concurrently.You can use this command to find number of available processors Runtime.getRuntime().availableProcessors()

Ryuzaki L
  • 37,302
  • 12
  • 68
  • 98
  • Thank you. But how does using a custom Executor bean, setting the core pool size to say, 100, change this? Because using the Executor and setting the high core pool size, the tasks don't wait to be picked up, and are instantly processed. – IceMan Sep 19 '19 at 16:23
  • Thread pool is something to gain advantage by running tasks parallely, if you want to run more threads parallely you need more cpu resource @IceMan – Ryuzaki L Sep 19 '19 at 16:35