Concurrency: แบบฉบับ Go
โมเดล Concurrency ของ Go
หัวข้อที่มีชื่อว่า “โมเดล Concurrency ของ Go”ภาษาส่วนใหญ่เพิ่ม concurrency เข้าไปบน shared-memory threads ภายหลัง แต่ Go ถูกออกแบบตั้งแต่ต้นรอบแนวคิดที่แตกต่างออกไปเรียกว่า Communicating Sequential Processes (CSP) ซึ่ง Tony Hoare เป็นผู้นิยามในปี 1978 คำพูดของ Go ที่สรุปได้ชัดที่สุดคือ:
อย่าสื่อสารโดยการแชร์หน่วยความจำ ให้แชร์หน่วยความจำโดยการสื่อสารแทน
ในทางปฏิบัติหมายความว่า:
- Goroutines คือ function ที่ทำงานอิสระ ใช้ทรัพยากรน้อยมาก — stack เริ่มต้นเพียงไม่กี่ KB ที่ขยายตัวได้ตามต้องการ — จึงสามารถมีได้หลายแสนตัวที่รันพร้อมกัน
- Channels คือท่อส่งข้อมูลแบบ typed goroutines ส่งค่าเข้า channel และรับค่าจาก channel อีกตัว ทั้งการส่งข้อมูลและการ synchronize เกิดขึ้นในคำสั่งเดียว
- Scheduler จัดสรร goroutines ลงบน OS threads (GOMAXPROCS threads ตามค่าเริ่มต้น หนึ่งตัวต่อ CPU core) คุณไม่ต้องจัดการ threads โดยตรง
การออกแบบนี้ทำให้โปรแกรม concurrent เข้าใจง่ายขึ้น เมื่อข้อมูลไหลผ่าน channel ความเป็นเจ้าของ (ownership) ชัดเจน: sender เคยถือมันอยู่ ตอนนี้ receiver ถือแทน ไม่ต้องใช้ mutex สำหรับการส่งต่อนั้นเลย
flowchart LR
subgraph SharedMemory["Shared-memory style (most languages)"]
T1["Thread A"] -.Lock/Unlock.-> Mu["mutex"]
T2["Thread B"] -.Lock/Unlock.-> Mu
Mu --> Data["shared variable"]
end
subgraph CSP["Go's CSP style"]
G1["Goroutine A"] -->|send| Ch["channel"]
Ch -->|receive| G2["Goroutine B"]
end โมดูลนี้ครอบคลุมอะไรบ้าง
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไรบ้าง”| บทเรียน | หัวข้อ |
|---|---|
| หน้านี้ | ภาพรวมโมเดลและตัวอย่างแรกที่รันได้ |
| Goroutines | go f(), scheduler, sync.WaitGroup |
| Channels | สร้าง, ส่ง, รับ, unbuffered vs buffered, close, range |
| Select | รวม channels หลายตัว, default, timeouts |
| Sync Primitives | sync.Mutex, sync.Once, sync/atomic |
| Context | context.Context, การยกเลิก, การส่งต่อ |
| Patterns | Worker pool, fan-out/fan-in, pipeline |
โปรแกรม concurrent ตัวแรกของคุณ
หัวข้อที่มีชื่อว่า “โปรแกรม concurrent ตัวแรกของคุณ”snippet ด้านล่างสร้าง goroutines 3 ตัวแบบขนาน แต่ละตัวเขียนผลลัพธ์ลงใน slice ที่จัดสรรไว้ล่วงหน้า indexed ตาม id ของตัวเอง ทำให้ผลลัพธ์เรียงลำดับเสมอ sync.WaitGroup บล็อก main จนกว่าทั้งสามตัวจะเสร็จสิ้น — บท Goroutines สอน WaitGroup เองแบบเจาะลึก (รวมถึงบัคคลาสสิกเรื่องตำแหน่ง wg.Add) หน้านี้แค่ต้องการมันเพื่อให้ main มีชีวิตอยู่นานพอที่จะเห็น output เท่านั้น
package main
import ( "fmt" "sync")
func main() { var wg sync.WaitGroup results := make([]string, 3)
for i := 0; i < 3; i++ { wg.Add(1) go func(id int) { defer wg.Done() results[id] = fmt.Sprintf("goroutine %d says hello", id) }(i) }
wg.Wait() for _, r := range results { fmt.Println(r) } fmt.Println("main exits")} ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| goroutine vs OS thread | 2-8 KB stack vs 1-8 MB | scheduler overhead |
| CSP (channel) vs shared memory | ชัดเจนว่า data flow ไปไหน | channel overhead สูงกว่า mutex |
| M:N scheduling | ใช้ OS thread น้อย | scheduler complexity |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- goroutine = OS thread — goroutine เบากว่า thread มาก สร้างได้หลักแสนตัว
- concurrent = parallel — concurrent คือจัดการหลาย task พร้อมกัน, parallel คือรันพร้อมกันจริงบน multi-core
- channel เสมอดีกว่า mutex — mutex เหมาะกับ simple shared state, channel เหมาะกับ data flow
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ทุกบทในโมดูลนี้จบด้วย section แบบเจาะลึกของตัวเอง นี่คือเวอร์ชันภาพรวมที่ควรเช็กตั้งแต่แรกเห็น ก่อนจะลงลึกในรายละเอียดของ snippet ที่ generate มา:
main(หรือ goroutine ที่เรียก) รอ goroutine ทุกตัวที่มันเริ่มไหม — ด้วยWaitGroup, channel, หรือ return channel ที่ caller drain — หรือมันอาจ return ทั้งที่ goroutine ยังรันอยู่?- ทุก goroutine มีทางออกไหม และมีอะไรป้องกัน state ที่ goroutine มากกว่าหนึ่งตัวแตะไหม?
- ถ้า snippet ถูกนำเสนอว่า deterministic ลำดับที่ print ออกมาถูกบังคับจริงไหม (sort แล้ว หรืออ่านกลับผ่าน indexed slice) หรือสองรอบที่รันอาจ print ลำดับต่างกันได้จริง ๆ?
🐛 หา bug ในโค้ดนี้
func main() { for i := 0; i < 3; i++ { go func(id int) { fmt.Println("goroutine", id, "says hello") }(i) } fmt.Println("main exits")}เฉลย
ไม่มีอะไรตรงนี้รอให้ทั้งสาม goroutine รันก่อนที่ main จะ return เลย Go runtime ไม่ได้เก็บ process ให้มีชีวิตอยู่เพื่อ background goroutine — ทันทีที่ main return process จะ exit ไม่ว่า goroutine ไหนจะถูก schedule ไปแล้วหรือยังก็ตาม output จริงที่จับได้จากสองรอบที่รันโปรแกรมเดียวกันเป๊ะนี้:
main exitsmain exitsgoroutine 0 says helloทั้งสองเป็นผลลัพธ์ที่ถูกต้องของ binary เดียวกัน — บางครั้งไม่มี goroutine ไหนได้รันเลย บางครั้งหนึ่งหรือสองตัวได้รัน ขึ้นอยู่กับ scheduling ล้วน ๆ วิธีแก้คือ sync.WaitGroup ที่ตัวอย่างที่รันได้ของหน้านี้เองใช้: main ต้องไม่ return จนกว่าจะยืนยันแล้วว่า goroutine ทุกตัวที่มันเริ่มเสร็จสิ้นแล้ว