กับดัก Go Channel: แค่ส่งข้อมูลลงท่อ ทำไมโปรแกรมถึงหยุดหายใจจน Deadlock?
⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 15 วินาที
คุณเพิ่งหัดเขียน Go และได้ยินสโลแกนสุดเท่ที่ว่า: "Don't communicate by sharing memory; instead, share memory by communicating."
คุณเลยลองสร้าง Channel ขึ้นมาตัวหนึ่ง หวังจะใช้ส่งข้อความแบบชิลๆ:
ch <- 42 // ส่งเลข 42 เข้า Channel
val := <-ch
fmt.Println(val)
พอสั่ง go run main.go แทนที่จะได้เลข 42 หน้าจอกลับพ่นสีแดงแป๊ดออกมาทันที:
fatal error: all goroutines are asleep - deadlock!
เกิดอะไรขึ้น? แค่ส่งเลขตัวเดียวลงไปในท่อ ทำไมถึงกลายเป็นหายนะระดับชาติที่ทำให้ Go Runtime สั่งชัตดาวน์ระบบตัวเอง?
🤝 ท่อไร้กล่องพัก: การส่งของแบบตัวต่อตัว (Rendezvous)
เวลาคุณประกาศ make(chan int) โดยไม่ใส่ตัวเลขขนาด Go จะสร้างสิ่งที่เรียกว่า Unbuffered Channel
หลายคนมักเข้าใจผิดว่า Channel คือ "กล่องไปรษณีย์" ที่เราเอาของไปหย่อนทิ้งไว้แล้วเดินหนีได้... แต่ความจริงคือ Unbuffered Channel ไม่มีกล่องเก็บของแม้แต่ช่องเดียว!
"มันทำงานเหมือน 'การยื่นของส่งต่อมือต่อมือ' ถ้าคนส่ง (Sender) ยื่นมือออกไป แต่ตรงหน้ายังไม่มีคนรับ (Receiver) มาจับของ... คนส่งจะถูกแช่แข็ง (Block) ค้างอยู่ในท่านั้นทันทีจนกว่าจะมีคนมารับ!"
ในโค้ดข้างบน ทั้งคนส่งและคนรับดันอยู่ใน Main Goroutine เดียวกัน:
- Main Goroutine สั่งส่งของ
ch <- 42➔ แต่มันไม่มีใครรอรับอยู่ตรงหน้า - Main Goroutine เลยโดนสั่ง Freeze แช่แข็งตัวเองรอคนมารับ
- แต่คนที่จะมารับ (บรรทัด
val := <-ch) ดันอยู่บรรทัดถัดไปของตัวเอง ซึ่งไม่มีวันรันไปถึงเพราะตัวเองกำลังโดนแช่แข็งอยู่!
🚨 สายสืบ Runtime: เมื่อทุกคนหลับหมดบ้าน
Go Runtime มีระบบตรวจจับ Deadlock อัตโนมัติ (Deadlock Detector)
ถ้ามันชะโงกหน้ามาดูแล้วพบว่า Goroutine ทุกตัวในระบบกำลังติดบล็อกหลับกันหมด (Asleep) และไม่มี Goroutine ไหนเหลืออยู่ที่จะมาปลุกใครได้อีก... Runtime จะรู้ทันทีว่าโปรแกรมนี้ตกอยู่ในลูปนรกชั่วนิรันดร์ มันจึงชิงสั่ง Crash ทิ้งทันที ดีกว่าปล่อยให้ระบบค้างกินไฟไปเรื่อยๆ
🛡️ 2 ทางออก: แยกเพื่อนร่วมงาน หรือ วางกล่องพักของ
1. ส่งงานให้คนละคนทำจริง (ใช้ Goroutine แยก):
// ให้ช่างอีกคนมารอรับของใน Goroutine ใหม่
go func() {
val := <-ch
fmt.Println(val)
}()
ch <- 42 // ส่งของได้ฉลุย เพราะมีคนรอจับของอยู่แล้ว!
2. ซื้อกล่องพักของมาวาง (Buffered Channel):
ch := make(chan int, 1)
ch <- 42 // วางของลงกล่องแล้วเดินหน้าต่อไปได้ทันที ไม่โดนแช่แข็ง!
val := <-ch
fmt.Println(val)
💡 5-Min Takeaway
"Unbuffered Channel คือการนัดส่งของแบบเจอหน้ากันตรงๆ ไม่ใช่ตู้รับจดหมาย" การเข้าใจว่า Unbuffered Channel บังคับ Synchronization สองฝั่งพร้อมกันเสมอ จะช่วยป้องกันบั๊กประเภท Deadlock ตั้งแต่ตอนออกแบบ Concurrency ใน Go และช่วยให้คุณตัดสินใจได้ถูกต้องว่าจะยอมให้ระบบ Block รอคนรับ หรือจะใส่ Buffer เพื่อผ่อนแรงการทำงาน