การประมวลผลแบบอะซิงโครนัสใน Java
ก่อนที่จะดู Spring @Async เราจำเป็นต้องเข้าใจแนวคิดของการประมวลผลแบบซิงโครนัส อะซิงโครนัส และมัลติเธรดก่อน ซึ่งเราจะถือว่าผู้อ่านมีความรู้พื้นฐานในส่วนนี้แล้ว และจะเริ่มต้นด้วยการดูวิธีการประมวลผลแบบอะซิงโครนัสใน Java แบบพื้นฐานผ่านโค้ด หากผู้อ่านมีความคุ้นเคยกับเธรดของ Java แล้ว สามารถข้ามบทนี้ไปได้
หากเราต้องการสร้างฟังก์ชันการพิมพ์ข้อความแบบซิงโครนัส เราสามารถเขียนโค้ดได้ดังตัวอย่างข้างต้น การเปลี่ยนโค้ดนี้ให้เป็นแบบอะซิงโครนัสด้วยมัลติเธรด สามารถทำได้ดังนี้
แต่การใช้วิธีนี้มีความเสี่ยงสูงเนื่องจากเราไม่สามารถจัดการเธรดได้ ตัวอย่างเช่น หากมีการเรียกใช้ฟังก์ชัน 10,000 ครั้งพร้อมกัน ระบบจะต้องสร้างเธรด 10,000 ตัวในเวลาอันสั้น ซึ่งการสร้างเธรดนั้นใช้ทรัพยากรค่อนข้างมาก อาจส่งผลเสียต่อประสิทธิภาพของโปรแกรม และอาจเกิดข้อผิดพลาด OOM ได้ ดังนั้น เราจึงต้องใช้ Thread Pool ในการจัดการเธรด และ Java มี ExecutorService class ไว้ให้บริการ
เราจำกัดจำนวนเธรดทั้งหมดไว้ที่ 10 และสามารถประมวลผลแบบอะซิงโครนัสด้วยมัลติเธรดได้อย่างถูกต้อง แต่การใช้วิธีนี้จะต้องใช้ submit() method ของ ExecutorService ในแต่ละ method ที่ต้องการประมวลผลแบบอะซิงโครนัส ทำให้ต้องแก้ไขโค้ดซ้ำๆ นั่นคือ หากเราต้องการเปลี่ยน method ที่เขียนด้วยลอจิกแบบซิงโครนัสให้เป็นแบบอะซิงโครนัส เราจะต้องแก้ไขลอจิกของ method นั้นๆ
Spring @Async
วิธีการง่ายๆ
เพียงแค่เพิ่ม @EnableAsync annotation บน Application class และเพิ่ม @Async annotation บน method ที่ต้องการประมวลผลแบบอะซิงโครนัส เท่านี้ก็เสร็จแล้ว แต่การใช้วิธีนี้มีปัญหาคือ ไม่ได้มีการจัดการเธรด เนื่องจากการตั้งค่าเริ่มต้นของ @Async คือการใช้ SimpleAsyncTaskExecutor ซึ่งไม่ใช่ thread pool แต่ทำหน้าที่สร้างเธรดขึ้นมาเท่านั้น
วิธีการใช้ Thread Pool
ก่อนอื่น ให้ลบ @EnableAsync ออกจาก Application class เนื่องจาก Application class มีการตั้งค่า @EnableAutoConfiguration หรือ @SpringBootApplication ซึ่งในขณะรันไทม์จะอ่านข้อมูล bean ของ threadPoolTaskExecutor (ซึ่งเราจะสร้างขึ้นในภายหลัง) จาก SpringAsyncConfig class (ซึ่งเราจะสร้างขึ้นในภายหลัง)
เราสามารถกำหนด core และ max size ได้ แต่สิ่งที่เราคาดหวังไว้ว่า เมื่อเริ่มต้นระบบจะมีเธรดทำงาน core size ตัว และเมื่อทำงานเกิน core size จะเพิ่มเธรดขึ้นไปจนถึง max size นั้นไม่เป็นความจริง
ภายในระบบนั้นจะมีการสร้าง LinkedBlockingQueue ที่มีขนาด Integer.MAX_VALUE โดยค่าเริ่มต้น เมื่อ core size ไม่สามารถประมวลผลงานได้ทัน จะนำงานไปใส่ใน Queue ก่อน และเมื่อ Queue เต็ม ก็จะเพิ่มเธรดขึ้นไปจนถึง max size เพื่อประมวลผล
หากเราไม่ต้องการให้ Queue มีขนาดใหญ่ถึง Integer.MAX_VALUE สามารถกำหนด queueCapacity ได้ ดังตัวอย่างข้างต้น เมื่อเริ่มต้นระบบจะมีเธรด 3 ตัวทำงาน และเมื่อทำงานเกิน 3 ตัว งานจะถูกนำไปใส่ใน Queue ขนาด 100 และเมื่อ Queue เต็ม จะเพิ่มเธรดขึ้นไปจนถึง 30 ตัว
เมื่อตั้งค่า Thread Pool เสร็จแล้ว เราสามารถกำหนดชื่อ bean ที่เราสร้างไว้ใน method ที่มี @Async annotation
หากต้องการกำหนด Thread Pool หลายๆ แบบ สามารถสร้าง method สร้าง bean เหมือนกับ threadPoolTaskExecutor() ได้หลายๆ method และกำหนดชื่อ bean ใน @Async ที่ต้องการใช้
รูปแบบการส่งคืนตามชนิดของค่าส่งคืน
กรณีที่ไม่มีค่าส่งคืน
กรณีที่ method ที่ต้องการประมวลผลแบบอะซิงโครนัสไม่จำเป็นต้องส่งผลลัพธ์กลับมา ในกรณีนี้ เราสามารถกำหนดชนิดของค่าส่งคืนของ @Async annotation เป็น void
กรณีที่มีค่าส่งคืน
สามารถใช้ Future, ListenableFuture, CompletableFuture เป็นชนิดของค่าส่งคืนได้ โดยเราจะต้องห่อผลลัพธ์ของ method แบบอะซิงโครนัสด้วย new AsyncResult()
[Future]
future.get() จะบล็อกการทำงานและรอจนกว่าจะได้รับผลลัพธ์ ทำให้การทำงานเป็นแบบซิงโครนัส จึงไม่ค่อยมีประสิทธิภาพ โดยทั่วไปแล้ว Future จะไม่ค่อยถูกนำมาใช้
[ListenableFuture]
ListenableFuture ช่วยให้เราสามารถประมวลผลงานได้แบบ non-blocking ผ่าน callback method addCallback() method ตัวแรกเป็น callback method ที่ทำงานเมื่อเสร็จสิ้น และตัวที่สองเป็น callback method ที่ทำงานเมื่อเกิดข้อผิดพลาด หมายเหตุ เนื่องจากเราตั้งค่า core thread ของ thread pool ไว้ที่ 3 ดังนั้นจึงเห็นข้อความ “Task Start” 3 ข้อความแรก
[CompletableFuture]
ถึงแม้ว่า ListenableFuture จะช่วยให้เราสามารถสร้างลอจิกแบบ non-blocking ได้ แต่หากจำเป็นต้องใช้ callback ซ้อน callback อาจทำให้เกิดโค้ดที่ซับซ้อนและอ่านยาก เรียกว่า callback hell
<span class="image-inline ck-widget" contenteditable="false"><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F9f152db8-c015-43cd-bf13-85c594f5f218%2FUntitled.png?table=block&id=268ac0bc-ca7b-4bcb-b11b-ac611a5038b2&spaceId=b453bd85-cb15-44b5-bf2e-580aeda8074e&width=2000&userId=80352c12-65a4-4562-9a36-2179ed0dfffb&cache=v2" alt="Untitled" style="aspect-ratio:2000/1455;" width="2000" height="1455"></span>
เนื่องจากหัวข้อนี้ไม่ได้เจาะลึกถึง CompletableFuture จึงขอแนะนำให้ผู้อ่านที่สนใจศึกษาโค้ดการแก้ปัญหา callback ซ้อน callback ดูจากลิงก์ด้านล่าง
โค้ดมีความอ่านง่ายกว่าการกำหนด callback ใน ListenableFuture และสามารถทำงานแบบ non-blocking ได้อย่างสมบูรณ์ ดังนั้น จึงแนะนำให้ใช้ CompletableFuture เมื่อต้องการค่าส่งคืนจาก @Async
ข้อดีของ @Async
ผู้พัฒนาสามารถเขียน method ด้วยลอจิกแบบซิงโครนัส และเมื่อต้องการเปลี่ยนเป็นแบบอะซิงโครนัส เพียงแค่เพิ่ม @Async annotation บน method เท่านั้น ทำให้โค้ดมีความยืดหยุ่นและง่ายต่อการดูแลรักษา
ข้อควรระวังของ @Async
ในการใช้ฟังก์ชัน @Async เราจำเป็นต้องประกาศ @EnableAsync annotation และหากไม่ได้มีการกำหนดค่าเพิ่มเติม ระบบจะทำงานในโหมด proxy นั่นคือ method แบบอะซิงโครนัสทั้งหมดที่ใช้ @Async จะต้องปฏิบัติตามข้อจำกัดของ Spring AOP สามารถศึกษาข้อมูลเพิ่มเติมได้จากโพสต์นี้
- การเพิ่ม @Async บน method ที่มี access modifier เป็น private จะไม่สามารถใช้งาน AOP ได้
- การเรียกใช้ method ภายใน object เดียวกัน จะไม่สามารถใช้งาน AOP ได้
ความคิดเห็น0