มารู้จักกับ Kotlin Coroutines ในเบื้องต้นกัน

Wattanachai Prakobdee
LINE Developers Thailand
4 min readFeb 8, 2019

Asynchronous หรือ non-blocking programming นั้นเริ่มมีบทบาทสำคัญขึ้นเรื่อยๆในโลกของ web APIs, desktop หรือ mobile applications การเขียนโปรแกรมแบบดั่งเดิมหรือ synchronous ที่ thread ถูกบล็อกในขณะที่รอ response นั้นอาจจะไม่เหมาะกับ platforms ที่ threads มีการใช้ทรัพยากรที่ค่อนข้างสูง เช่น JVM หรือ mobile application ซึ่งก็มีอยู่หลายวิธีที่จะช่วยในการแก้ไขปัญหานี้ ใน Kotlin เองก็มี coroutines ซึ่งเป็น feature ที่มาช่วยให้เราเขียนโปรแกรมแบบ asynchronous หรือ non-blocking code ใน structure ของ synchronous code ซึ่งมี support ที่ language level ในบทความนี้ เราจะมาทำความรู้จักกับ coroutines ในเบื้องต้นไปพร้อมๆกันครับ

What are Coroutines ?

ตาม document ของ Kotlin นั้น ได้นิยาม coroutines ไว้ว่า coroutines คือ lightweight threads ซึ้งจริงๆแล้ว coroutines ก็เหมือนกับ threads แต่ว่ามีข้อดีกว่า threads อยู่มาก เช่น อย่างแรก ด้วย coroutines ทำให้เราสามารถเขียน asynchronous code ในรูปแบบของ synchronous code เช่น

สมมติว่าเรามี code ที่ทำอะไรบางอย่าง ดังนี้

ด้วย coroutines เราสามารถเขียน code ในรูปแบบ sequentially หรือ synchronous structure ดังนี้

จะเห็นว่าด้วย coroutines เรามารถเขียน asynchronous code ได้เหมือนกันกับที่เราเขียน code ตามปกติ

ปล. สำหรับใครที่สงสัยว่า suspend คืออะไร ตอนนี้ขอให้เราละไว้ก่อน เดี่ยวเราจะมีทำความรู้จักกับ suspend ในภายหลังกันครับ

สำหรับตัวอย่างที่สองนั้น coroutines มีประสิทธิภาพที่สูงกว่า thread ซึ่งจริงๆแล้ว coroutines จำนวนมากมาย สามารถ executed ภายใน thread เดียวกัน นั้นจึงเป็นเหตุผลว่า ทำไม application ของเราถึงใช้งาน thread โดยมีข้อจำกัด แต่สำหรับ coroutines แล้ว เราสามารถใช้งาน coroutines ในจำนวนเท่าไรก็ได้เท่าที่เราต้องการ เกือบจนจะเป็น no limit เลยทีเดียวครับ เพื่อเป็นการพิสูจน์นั้น ลองทำการ run code ดังต่อไปนี้

จาก code ด้านบน เป็นการ launches coroutines จำนวน 1 ล้าน coroutines ถ้าเราลอง code ด้านบนด้วย threads นั้น เราอาจจะได้ out of memory exception หรือถ้าเราใช้ computor ที่มีประสิทภาพสูง ก็อาจจะใช้เวลาในการประมวลผลช้ามากๆเลยทีเดียว ถ้าไม่เชื่อให้เราลองเปลี่ยนจาก launch เป็น threads แล้วลอง run ใหม่ดูครับ

เอาหละครับ จากตัวอย่าง code ก่อนหน้านี้ เราอาจจะสังเกตเห็น suspend modifier กันแล้ว ซึ่งจริงๆแล้ว coroutines มีพื้นฐานแนวคิดมาจาก suspending functions ซึ่ง suspending เป็น functions ที่สามารถหยุดการทำงานของ coroutine ณ จุดเวลาใดเวลาหนึ่ง ( ทำงานโดยไม่ block thread ปัจจุบัน ) จากนั้นสามมารถกลับมา control coroutine อีกครั้ง เมื่อ function ทำงานเสร็จเรียบร้อยแล้วและได้ result ที่พร้อมใช้งาน

Suspending functions

อะไรคือ suspend functions ใน coroutines กันแน่ ถ้า function ถูก suspended แล้วใครเป็นคนควบคุม function นั้นๆ แล้ว suspended function จะถูก resume กลับมาทำงานต่อได้ยังไง ใครเป็นคน resume function นั้นๆ หลายคนอาจจะมีข้อสงสัยเต็มไปหมด เมื่อเราพูดถึง coroutines แล้ว suspend functions เป็นเหมือนหัวใจสำคัญของ coroutines ฉนั้น suspend functions จึงสำคัญมากๆ อย่างที่เรารู้ไปแล้วว่า เวลาที่เรา suspend function นั้น coroutines จะทำการ executed โดยจะไม่ทำการ blocking thread ปัจจุบัน

เอาหละครับ เรามาทำความรู้จักกับ idea การทำงานของ suspend functions กันดีกว่า ลองพิจารณา code ดังต่อไปนี้

ถ้าเรา run code ด้านบน จะได้ output ดังนี้

one
The value 1
two
The value 2
three
The value 3
done

จาก code ด้านบนนั้น ในครั้งแรกที่เราเรียก function นั้น ตัว function จะ run จนถึงบรรทัดที่ 4 แล้ว return ค่า 1 ออกมา และในการเรียกครั้งที่ 2 นั้น การทำงานจะไม่เหมือน function ทั่วไป โดย function จะเริ่มทำงานต่อในบรรทัดที่ 5 แล้ว return ค่า 2 ในบรรทัดที่ 7 ออกมา แต่ละครั้งที่การทำงาน yield function จะทำการ return ตัวเลขถัดไป และ continuation จะทำการ wrap code หรือจำในส่วนที่เหลือใน function ไว้ด้วย ลักษณะการทำงานแบบนี้นั้น ถือเป็นการ suspended function นั้นเอง เมื่อ function เรียกใช้ตัวเลขถัดไป ตัว continuation จะใช้ในการ resume ในส่วนที่เหลืออยู่ของ function ไปเรื่อยๆนั้นเอง จากตัวอย่างด้านบนเป็นแนวคิดให้เราเห็นภาพลักษณะการทำงานของ suspend functions นั้นเองครับ

เพื่อที่จะสรุปให้เห็นภาพชัดเจน ความแตกต่างของ suspend functions กับ function ปกตินั้น ตัว suspend functions สามารถเริ่ม หยุด และทำงานต่อ… หยุด และทำงานต่อ…ถ้าต้องการไปเรื่อยๆ จนกว่าจะสิ้นสุดนั้นเองครับ

Coroutine Builders

suspending function นั้น สามารถเรียกใช้งานได้จาก suspending function เหมือนกันหรือเรียกใช้งานจาก coroutine เท่านั้น ดังนั้นจาก function ปกติ ถ้าเราจะเรียกใช้งาน suspending function เราจึงต้องใช้ coroutine builders ช่วยในการเรียกใช้ suspending function โดย coroutine builders จะช่วยในการ provide CoroutineScope ให้เรา ตัวอย่างของ coroutine builders เช่น runBlocking, launch และ async ที่เราได้เห็นไปบ้างแล้วจาก code ก่อนหน้านี้นั้นเองครับ

  • runBlocking จะทำการเริ่ม coroutine ใหม่ พร้อมทำการ blocks thread ปัจจุบันจนกว่า coroutine จะทำงานเสร็จ ซึ่งทำหน้าที่เป็นสะพานเชื่อมระหว่าง synchronous code ปกติกับ asynchronous code นั้นเองครับ
  • launch จะทำการเริ่ม coroutine ใหม่ โดยที่ไม่มีการ blocking thread ปัจจุบัน และจะทำการ return coroutine ในรูปแบบของ job ซึ่งเราสามารถเรียก join() จาก job ที่ return ออกมา เพื่อที่จะรอให้ coroutine ทำงานจนจบ
  • async จะทำการเริ่ม coroutine ใหม่ โดยไม่ทำการ blocking thread ปัจจุบันเหมือนกันกับ launch แต่ความต่างระหว่าง launch กับ async นั้น async จะ return result ของ coroutine ในรูปแบบของ Deferred ซึ่งเราสามารถ get เอาค่า result ที่แท้จริงโดยเรียก await() นั้นเองครับ

Coroutine Scope

จากที่เรารู้ไปแล้วว่า coroutine ทำงานภายใน scope ที่เราทำงานอยู่ ณ ขณะนั้น ทุก coroutine builders ไม่ว่าจะเป็น runBlocking, launch และ async นั้น ล้วน extension บน coroutineScope ทั้งนั้น เราสามารถประกาศ scope โดยใช้ scoping function เช่น coroutineScope, GlobalScope , withContext เป็นต้น

จาก code ด้านล่างเป็นการประกาศ scope ใหม่ โดยใช้ coroutineScope builder

ถ้าเรา run code ด้านบนจะได้ output ดังนี้

นี้เป็นเพราะว่า coroutine scopes ถูกสร้างโดย launch และ coroutineScope builder ไม่ได้ทำการ block thread ปัจจุบันในขณะที่รอ children coroutines ทำงานจนเสร็จนั้นเอง

Coroutine Context

CoroutineContext เป็น optional parameter ของ coroutine builders ทุกตัว ซึ่งจะถูก inherite มาจาก CoroutineScope ถ้าเราไม่ได้ระบุ coroutine Context ตอนที่สร้าง coroutine builders

launch {
// context ตรงนี้จะเป็นของ parent
}

elements ที่เราจะเห็นบ่อยๆของ coroutineContext จะประกอบไปด้วย

  • Job เป็น object ที่ทำหน้าที่เป็น background operation และ lifecycle ซึ่งปกติแล้วจะ return มาจาก coroutine builders ด้วย job ที่ return มา เราสามารถตรวจสอบการทำงานของ coroutine และยังสามารถ cancel coroutine ได้ด้วย
  • ContinuationInterceptor เป็น coroutineContext elements ที่ทำหน้าที่ intercept continuation ของ coroutine เช่น Coroutine Dispatchers
  • CoroutineName ใช้ในการระบุชื่อของ coroutine
  • CoroutineExceptionHandler เป็น exception handler ที่ใช้ในการจัดการกับ exceptions ที่ไม่ได้ถูกตรวจจับใน context ปัจจุบัน

Coroutine Dispatchers

CoroutineDispatcher นั้นเป็น continuation interceptors และ coroutine context element ที่สามารถใช้ในการควบคุมการทำงานของ coroutine เราสามารถใช้ในการระบุขอบเขตของ coroutine ให้กับ thread หรือว่า thread pool ที่เราต้องการ หรือว่าจะไม่ระบุขอบเขตของ coroutine เลยก็ได้ด้วยเช่นกัน หมายความว่า CoroutineDispatcher สามารถระบุได้ว่าเราจะให้ thread หรือ thread pool ไหนรับผิดชอบในการทำงานของ coroutine นั้นเอง

kotlinx.coroutines มาพร้อมกับ dispatchers พื้นฐานต่างๆ ดังนี้

  • Dispatchers.Default เป็น CoroutineDispatcher ที่ใช้โดยทุก coroutine builders เช่น launch, asyn ถ้าเราไม่ได้ระบุ dispatcher ใน context และ Dispatchers.Default จะใช้ shared pool ของ threads บน JVM โดยปกติแล้ว level สูงสุดของการทำ parallelism ของ Dispatchers.Default จะเท่ากับจำนวน cores ของ CPU
  • Dispatchers.IO ใช้สำหรับ blocking IO tasks เช่น file I/O และ socket I/O โดยใช้งาน shared pool ของ threads บน JVM เหมือนกันกับ Default dispatcher
  • Dispatchers.Unconfined เป็น dispatcher ที่ run coroutine แบบที่ไม่ได้กำหนด thread ใดๆ โดย coroutine จะทำงานใน thread ปัจจุบัน แต่จะถูก resume โดย thread ใดก็ตามที่ใช้กับ suspending function ที่เป็น argument ของ coroutine builder
  • Dispatchers.Main เป็น dispatcher ที่ระบุการทำงานของ coroutine ให้ทำงานบน main thread หรือ UI thread

Coroutine Cancelation

ในการ cancel coroutine นั้น เราสามารถทำได้โดยการเรียก cancel() function จาก job ที่ return มาจาก coroutine builders

จาก code ด้านบน ในขณะที่ตัว body ของ runBlocking ได้ทำการ delays ในบรรทัดที่ 9 และ launched coroutine ได้ทำการ print “♥️ AOM BNK48” ออกมาทาง console 4 ครั้ง จากนั้นก็จะหยุดทำงานทันทีที่ cancel() ถูกเรียก

Exception Handling

ใน synchronous programming เราใช้ try catch ช่วยในการ handle exception และใน coroutines เอง เราก็สามารถใช้ try catch ช่วยในการ handle exception ได้เหมือนกันกับ synchronous code เช่น

Summary

ถึงตรงนี้เราอาจพอจะเห็นภาพแล้วว่า coroutines เป็นรูปแบบการเขียน asynchronous code ที่สามารถเขียนในรูปแบบของ synchronous code ที่ไม่ใช่แค่ใช้งานง่ายเพียงอย่างเดียว coroutines ยังมาพร้อมกับประสิทธิภาพที่สูง โดยเฉพาะอย่างยิ่งถ้าใช้กับ mobile application ที่การใช้งาน threads จำนวนมากสามารถส่งผลให้เกิด performance issues ได้

บทความนี้เราก็ได้รู้จักกับ coroutines และ elements ต่างๆของ coroutines ไปแบบคร่าวๆ อย่างไรก็ตาม ถ้าใครสนใจอยากศึกษาเพิ่มเกี่ยวกับ coroutines สามารถหาอ่านเพิ่มเติมได้ที่ documentation

--

--

Wattanachai Prakobdee
LINE Developers Thailand

Software Engineer at LINE Thailand | Learning is a journey, Let's learn together.