ข้าม​ไป​ยัง​เนื้อหา

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(...): Longpublic static final java.lang.Object quote(java.lang.String, kotlin.coroutines.Continuation<? super java.lang.Long>)
fun plain(...): Longpublic static final long plain(java.lang.String)

สอง​อย่าง​เปลี่ยน​พร้อม​กัน

  • ชนิด​คืน​ค่า​เปลี่ยน​จาก long เป็น Object · Long ถูก​ห่อ และ​ช่อง​นั้น​ยัง​ต้อง​รับ ค่า​พิเศษ​ที่​แปล​ว่า “ยัง​ไม่​เสร็จ” ได้​ด้วย
  • มี parameter เพิ่ม​มา​หนึ่ง​ตัว ชนิด Continuation ซึ่ง​ไม่มี​ใคร​เขียน​ไว้​ใน source

compiler สร้าง​มัน​มา​เก็บ​สถานะ​ระหว่าง​จุด​ที่​หยุด

$ ls
SuspKt$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

ตัวแปร​ท้อง​ถิ่น​ข้าม​จุด​หยุด​ได้ เพราะ​มัน​ไม่​ได้​อยู่​บน stack แล้ว

val base ใน​ตัวอย่าง​ถูก​ประกาศ​ระหว่าง delay สอง​ตัว · ถ้า function นี้​ทำงาน บน stack ตาม​ปกติ การ​หยุด​แล้ว​คืน thread ไป​ให้​คน​อื่น​จะ​ทำให้​ค่า​นั้น​หาย​ไป

J$0 คือ​ที่​ที่ base ไป​อยู่ · มัน​เป็น field ขอ​งอ็​อบ​เจ​กต์ ไม่ใช่​ช่อง​บน stack · นี่​คือ​เหตุผล​ที่ coroutine ไม่​ต้อง​จอง stack ของ​ตัว​เอง และ​เป็น​ข้อ​ที่​ต่าง​จาก thread จริง​มาก​ที่สุด

ชื่อ J มา​จาก​รหัส​ชนิด​ของ JVM สำหรับ long ส่วน L คือ reference · compiler แยก​ช่อง​ตาม​ชนิด​เพื่อ​ไม่​ต้อง​ห่อ​ค่า​พื้น​ฐาน​ทุก​ตัว

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.py
npm run verify:kotlin-bytecode

ช่อง​ควบคุม​ของ​หัวข้อ​นี้​คือ plain ซึ่ง​ประกาศ​คืน Long เหมือน​กัน · ถ้า​ลาย​เซ็น​ของ​มัน​เปลี่ยน​ไป​ด้วย แปล​ว่า​เรา​กำลัง​วัด​สิ่ง​อื่น ไม่ใช่​ผล​ของ suspend