Skip to main content

Command Palette

Search for a command to run...

Spring ก็เร็วได้! มาเรียก Method ใน Spring แบบ Parallel ด้วย @Async กันเถอะ

Published
5 min readView as Markdown
Spring ก็เร็วได้! มาเรียก Method ใน Spring แบบ Parallel ด้วย @Async กันเถอะ
Y

My name is Yothin Muangsommuk. I've been in many roles in Software Development team, Software Engineer, Tech Lead, and Technical Coach. My focus has always been improving a sustainability and maintainability through Technical Excellence and Software Craftsmanship. I also A Python enthusiasm, An occasional conference speaker (PyCon APAC, Code Mania, Barcamp Bangkok), and a community organizer (PyCon Thailand, PySomTum, GDG Cloud Bangkok).

Interests: Programming Language Design, Software Engineering, Distributed System

สมมติว่าเรามีโจทย์ที่ต้องยิง HTTP Request เพื่อดึง resource มาจาก Web service อื่นหลายๆ ที่ ถ้าเราไม่คิดอะไรเราก็อาจจะเขียนโค้ดให้ ยิงทีละตัวแบบ Synchronous ไปได้ แต่ถ้าแต่ละ Request ไม่ได้ขึ้นต่อกัน เราจะมีตัวเลือกมากขึ้นผ่านการยิงแบบ Asynchronous ครับ ซึ่งผมเพิ่งรู้ว่า Java มันทำได้

Example

สมมติว่าเรามี Service ที่ไว้ยิง HTTP Request ไปที่ API อื่นแบบง่ายๆ หน้าตาประมาณนี้

@Service
public class ActivityService {

    RestTemplate restTemplate = new RestTemplate();

    public ResponseSecond getEndpointOne() {
        String url = "http://localhost:8000/1";
        ResponseSecond response = restTemplate.getForObject(url, ResponseSecond.class);
        return response;
    }
}

และมี Controller ที่เรียกใช้ Service นี้หน้าตาประมาณนี้

@RestController
public class ActivityController {

    @Autowired
    ActivityService activityService;

    @GetMapping(path = "/")
    public ResponseEntity<String> getActivity() throws Throwable {
        try {
            ResponseSecond one = activityService.getEndpointOne();
            ResponseSecond two = activityService.getEndpointOne();
            ResponseSecond three = activityService.getEndpointOne();

            String response = "";
            if (one != null && two != null && three != null) {
                response = one.getMessage() + " " + two.getMessage() + " " + three.getMessage();
            }
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            throw e.getCause();
        }
    }

ก็ดูทำงานได้ปกติใช่มั้ยครับ แต่สิ่งที่เกิดคือ มันจะทำงานแบบ Synchronous ก็คือ getEndpointOne แต่ละตัวต้องถูกทำให้เสร็จก่อนที่จะเรียกตัวถัดไป ซึ่งสมมติว่าแต่ละ Endpoint ต้องทำงาน 1 วิ นั่นคือ 3 วิเลยกว่า controller นี้จะวิ่งจนครบ loop

ทำให้มัน Async เถอะ

อย่างแรกที่เราต้องแก้คือ Service ครับ โดยเราจะใช้ความสามารถของ Annotation ชื่อ @Async มาทำให้ Method ของเรานั้นสามารถที่จะเรียกแบบ Asynchronous ได้ แต่มีข้อแม้สองข้อครับคือ

  • Method ที่เราจะทำให้เป็น Async นั้นต้องเป็น public method
  • Method ที่เรียกใช้งาน Async Method นั้นต้องอยู่คนละ Class กันครับ เพราะ Async ทำ self-invocation ไม่ได้

พอรู้แบบนี้แล้ว ขั้นแรกคือเราต้องแปลง method ที่เราจะทำให้มันเป็น async ให้หน้าตาประมาณนี้ครับ

@Async
public CompletableFuture<ResponseSecond> getEndpointOne() {
    String url = "http://localhost:8000/1";
    ResponseSecond response = restTemplate.getForObject(url, ResponseSecond.class);
    return CompletableFuture.completedFuture(response);
}

จะเห็นว่ามีสิ่งที่เพิ่มขึ้นมา 3 อย่างคือ

  • @Async ที่เป็นการระบุว่า Method นี้สามารถเรียกใช้แบบ Asynchronous ได้
  • Return type signature จะถูกครอบด้วย CompletableFuture<T> ครับ
  • Return value จะต้องเพิ่ม CompletableFuture.completedFuture() ครอบไปครับ

ในส่วนของ method ที่เรียกใช้งาน Async method นั้นจะเปลี่ยนไปหน้าตาประมาณนี้ครับ

public ResponseEntity<String> getAsync() throws Throwable {
    try {
        CompletableFuture<ResponseSecond> one = activityService.getEndpointOne();
        CompletableFuture<ResponseSecond> two = activityService.getEndpointOne();
        CompletableFuture<ResponseSecond> three = activityService.getEndpointOne();

        CompletableFuture.allOf(one, two, three);
        String response = "";
        if (one != null && two != null && three != null) {
            response = one.get().getMessage() + " " + two.get().getMessage() + " " + three.get().getMessage();
        }
        return ResponseEntity.ok(response);
    } catch (Exception e) {
        throw e.getCause();
    }
}

จะเห็นว่าในส่วนที่เรียก method จะเปลี่ยน return type เป็น CompletableFuture<T> แทนครับ และจะมีโค้ดเพิ่มขึ้นมาคือ CompletableFuture.allOf(<async method>) ซึ่งจะทำหน้าที่เรียกและรอ return future value ให้เราจนกว่า async method ที่เรียกข้างในทำงานเสร็จทั้งหมดครับ ซึ่งหลังจากนั้นเราสามารถใช้ method get() เรียก CompletableFuture เพื่อดึงค่าออกมาจาก async method ได้เลย

แต่ถ้าเราเรียกตอนนี้จะเห็นว่ามันยังเรียกแต่ละ method แบบ sychronous กันอยู่ทำให้ใช้เวลารวมทั้งหมดอยู่ 3 วิ (ถ้าแต่ละ method ใช้ 1 วิ) ซึ่งเกิดจากการที่ Executor ของเรายังเป็น applicationTaskExecutor ซึ่งรันอยู่บน single thread อยู่ครับ โดยเราสามารถเปลี่ยนไปใช้ Executor แบบ Async โดยการเพิ่ม Async Config ตามตัวอย่างข้างล่างได้ครับ

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        return executor;
    }
}

ซึ่งพอถึงจุดนี้ใน Service ของเราก็จะทำงานผ่าน asyncExecutor แล้วครับ แต่ถ้าเราสั่งให้ controller เราทำงานจะเห็นว่ามันยังทำงานอยู่ที่ 3 วิ อยู่ ทำไมมันยังไม่เร็วขึ้นนะ?

corePoolSize vs maxPoolSize

สาเหตุที่มันยังทำงานแบบ synchronous อยู่ทั้งๆ ที่เราใช้ asyncExecutor เป็นเพราะว่ามันยังใช้ Thread เดียวอยู่ครับ ซึ่งถ้าเราไปดู Implementation ของ ThreadPoolTaskExecutor จะเห็นว่าเซต corePoolSize อยู่ที่ 1 เลยทำให้มันเป็น single thread แต่เราสามารถเพิ่มจำนวน thread ที่ stand by อยู่ได้ผ่าน method: setCorePoolSize ได้ โดยในที่นี้จะเซตเป็น 3 เท่าจำนวน tasks ที่เราจะแยก Thread ออกสูงสุดครับ

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(3);
        executor.setMaxPoolSize(5);
        return executor;
    }
}

อีกส่วนคือ maxPoolSize ซึ่งเป็นตัวกำหนดว่า Java จะแตก Thread ได้มากที่สุดกี่ตัวครับ ซึ่ง default จะเป็นเท่ากับจำนวน Integer เลย ซึ่งเราควรจะกำหนดไว้เพื่อกันไม่ให้ Java แตก Thread ออกมามากเกินไปครับ ในที่นี้จะกำหนดเป็น 5

ซึ่งพอถึงจุดนี้ตัว Controller นี้จะทำงานอยู่ที่ประมาณ 1 วิ + นิดหน่อยครับ แล้วถ้าเราดูที่ service ปลายทางได้เราจะเห็นว่ามันยิง HTTP Request ออกมาแทบจะเวลาเดียวกันแล้วครับ

Reference

More from this blog

ยิง request แบบไม่มีสะดุดด้วย emacs REST client

ช่วงปีใหม่ผมมีเวลาไปเรียนคอร์ส The Ultimate Go Series ของพี่ยอดซึ่งจากใจคนที่ดู Go อยู่ห่างๆ ผมบอกได้เลยว่าคอร์สนี้สอนได้ครบเครื่องมากในเวลาไม่กี่ชั่วโมง แต่นั่นไม่ใช่ประเด็นครับ ประเด็นคือตอนคอร์ส 2 ที่กำลังเรียนเรื่อง API พี่ยอดใช้ HTTP Client ที่ร...

Jan 9, 20223 min read102
ยิง request แบบไม่มีสะดุดด้วย emacs REST client

[Book Review] The Phoenix Project เพราะ IT Operation ก็ไม่ต่างกับ Production Plant

ผมรู้จัก The Phoenix Project มานานมากและมันวนเวียนมาเกือบจะได้อ่านหลายรอบ จนกระทั่งเมื่อประมาณกลางเดือนที่แล้ว ผมเพิ่งได้มีโอกาสจะหยิบมันขึ้นมาอ่าน และตั้งแต่ตอนนั้นผมแทบจะวางไม่ลงเลยตลอดระยะเวลาเกือบหนึ่งเดือนที่ผ่านมา และผมแนะนำให้กับทุกคนที่ทำงานใ...

Dec 20, 20214 min read456
[Book Review] The Phoenix Project เพราะ IT Operation ก็ไม่ต่างกับ Production Plant

Emacs diary EP2: สภาพแวดล้อม

ผมไม่รู้ว่าจะมีใครมาใช้ emacs แบบ เป็น text-editor ตัวแรกเลยมั้ย ผมไม่ใช่คนนึงแน่ๆ ซึ่งพอเราเปลี่ยนเครื่องมือ มันจะอารมณ์เดียวกับเราเปลี่ยนภาษา Programming ที่เราเขียนทุกวันเลย เพราะสิ่งแรกๆ ที่เราพยายามทำคือ "เราพยายามจะทำสิ่งเดียวกับที่เราเคยทำได้จ...

Dec 4, 20214 min read98
Emacs diary EP2: สภาพแวดล้อม

เรื่องประหลาดๆ ของการนับปีใน Java

ลืมหัวข้อ blog นี้ไปก่อนนะครับ แล้วลองพยายามเดาดูว่าโค้ด Java ข้างล่างนี้พยายามทำอะไร มันก็ดูตรงไปตรงมาใช่มั้ยครับ แล้วถ้ารันออกมาสมมติวันนี้วันที่ 1 ธันวาคม 2021 สิ่งที่ function นี้คืนกลับมาก็ เป็น 2020-12-01 ใช่มั้ยครับ ถ้าเราเขียน Unit test ไว้ ...

Dec 1, 20212 min read96
เรื่องประหลาดๆ ของการนับปีใน Java

Make Time | โลกมันวุ่นวาย แต่เรายังหาความสงบได้นะ

เคยมีความรู้สึกแบบนี้มั้ยครับ อารมณ์ประมาณว่า อยากจะเรียนคอร์สนั้นที่ซื้อไว้ตั้งนานแล้วให้จบ อยากจะอ่านหนังสือเล่มที่ซื้อเมื่อเดือนก่อน อยากจะทำ side project ที่คิดไว้ให้ออกมาเป็นรูปเป็นร่าง อยากจะมีทำให้ความสัมพันธ์ในชีวิตกับคนที่เราแคร์ดีขึ้น อะไรแ...

Nov 18, 20212 min read144
Make Time | โลกมันวุ่นวาย แต่เรายังหาความสงบได้นะ
P

Python by Cow

22 posts