suspend ไม่ใช่เรื่องของ thread — มันเปลี่ยนลายเซ็นที่ JVM เห็น
คำอธิบายที่พบบ่อยที่สุดของ coroutine คือ “thread ที่เบา” · คำนั้นอธิบายผลลัพธ์ได้ แต่ไม่ได้อธิบายกลไก และมันพาให้เดาผิดเวลาโปรแกรมทำตัวไม่เหมือนที่คิด
กลไกจริงเห็นได้ด้วยคำสั่งเดียว
ลายเซ็นสองแบบ วางไว้ข้างกัน
หัวข้อที่มีชื่อว่า “ลายเซ็นสองแบบ วางไว้ข้างกัน”ประกาศคืน
Longทั้งคู่ ต่างกันแค่คำว่าsuspend
suspend fun quote(sku: String): Long { delay(1) val base = sku.length * 1000L delay(1) return base + 500L}
fun plain(sku: String): Long = sku.length * 1000L + 500L$ javap -p SuspKt| ประกาศใน Kotlin | ลายเซ็นที่ JVM เห็น |
|---|---|
suspend fun quote(...): Long | public static final java.lang.Object quote(java.lang.String, kotlin.coroutines.Continuation<? super java.lang.Long>) |
fun plain(...): Long | public static final long plain(java.lang.String) |
สองอย่างเปลี่ยนพร้อมกัน
- ชนิดคืนค่าเปลี่ยนจาก
longเป็นObject·Longถูกห่อ และช่องนั้นยังต้องรับ ค่าพิเศษที่แปลว่า “ยังไม่เสร็จ” ได้ด้วย - มี parameter เพิ่มมาหนึ่งตัว ชนิด
Continuationซึ่งไม่มีใครเขียนไว้ใน source
class ที่ไม่มีใครเขียน
หัวข้อที่มีชื่อว่า “class ที่ไม่มีใครเขียน”compiler สร้างมันมาเก็บสถานะระหว่างจุดที่หยุด
$ lsSuspKt$quote$1.class$ javap -p 'SuspKt$quote$1'class นี้สืบทอด kotlin.coroutines.jvm.internal.ContinuationImpl และมี field เหล่านี้
| field | ทำอะไร |
|---|---|
label | ตัวนับสถานะ · บอกว่ารอบถัดไปต้องกระโดดไปทำต่อที่ไหน |
result | ค่าที่ได้กลับมาจากจุดที่หยุดครั้งก่อน |
L$0 J$0 | ตัวแปรท้องถิ่นที่ต้องรอดข้ามจุดหยุด · แยกช่องตามชนิด |
field ที่ probe อ่านได้จริงคือ L$0 · J$0 · result · label
val base ในตัวอย่างถูกประกาศระหว่าง delay สองตัว · ถ้า function นี้ทำงาน
บน stack ตามปกติ การหยุดแล้วคืน thread ไปให้คนอื่นจะทำให้ค่านั้นหายไป
J$0 คือที่ที่ base ไปอยู่ · มันเป็น field ของอ็อบเจกต์ ไม่ใช่ช่องบน stack
· นี่คือเหตุผลที่ coroutine ไม่ต้องจอง stack ของตัวเอง และเป็นข้อที่ต่างจาก
thread จริงมากที่สุด
ชื่อ J มาจากรหัสชนิดของ JVM สำหรับ long ส่วน L คือ reference
· compiler แยกช่องตามชนิดเพื่อไม่ต้องห่อค่าพื้นฐานทุกตัว
label คือคำตอบว่าทำไมมันกลับมาทำต่อได้
หัวข้อที่มีชื่อว่า “label คือคำตอบว่าทำไมมันกลับมาทำต่อได้”function เดียวถูกเรียกหลายครั้ง และมันข้ามไปทำต่อจากที่ค้างไว้
quote ที่มี delay สองจุด ถูกเรียกจริงมากกว่าหนึ่งครั้ง
· รอบแรกทำถึง delay ตัวแรกแล้วคืน · รอบถัดมาอ่าน label แล้วกระโดดข้าม
บรรทัดที่ทำไปแล้ว ไปเริ่มที่จุดหลัง delay
ค่าที่มันคืนตอนยังไม่เสร็จ ไม่ใช่ Long · นั่นคือเหตุผลที่ชนิดคืนค่าต้องกว้างเป็น
Object · ผู้เรียกที่เป็น Kotlin ไม่เคยเห็นค่าตัวนั้น เพราะ compiler จัดการให้หมด
สิ่งที่ตามมาในทางปฏิบัติ
หัวข้อที่มีชื่อว่า “สิ่งที่ตามมาในทางปฏิบัติ”ทุกข้อข้างล่างอธิบายได้ด้วยลายเซ็นที่วัดไว้ข้างบน
- เรียก
suspend funจาก Java ตรงๆ ไม่ได้อย่างที่คิด เพราะสิ่งที่ Java เห็น คือ method ที่ต้องส่งContinuationเข้าไปเอง และคืนObject suspendไม่ทำให้อะไรทำงานพร้อมกัน มันแค่ทำให้ function หยุดแล้วกลับมาทำต่อได้ · ความพร้อมกันมาจากlaunchหรือasyncซึ่งเป็นคนละเรื่อง- การหยุดไม่ได้ผูกกับ thread ใดเป็นพิเศษ สถานะอยู่ในอ็อบเจกต์ ไม่ได้อยู่บน stack ของ thread · รอบถัดไปจึงเดินบน thread คนละตัวได้
suspendที่ไม่มีจุดหยุดจริง ไม่ได้ประหยัดอะไร มันยังจ่ายค่าลายเซ็นที่กว้างขึ้น และการห่อค่า
ทุกค่ามาจาก kotlinc 2.4.10 กับ javap ตัวจริง
· แปลด้วย -Werror -Wextra -jvm-target 25 และไม่มีหน่วยเวลาสักตัว
python3 scripts/kotlin-bytecode-facts/probe.pynpm run verify:kotlin-bytecodeช่องควบคุมของหัวข้อนี้คือ plain ซึ่งประกาศคืน Long เหมือนกัน
· ถ้าลายเซ็นของมันเปลี่ยนไปด้วย แปลว่าเรากำลังวัดสิ่งอื่น ไม่ใช่ผลของ suspend
- Kotlin — Coroutines basics (เข้าถึง 2026-08-30)
- Kotlin — Calling Kotlin from Java (เข้าถึง 2026-08-30)
- คอร์ส Kotlin จากศูนย์ บทที่ 8 — ใช้ coroutine จริงกับ virtual thread