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

Concurrency: แบบฉบับ Go

ภาษาส่วนใหญ่เพิ่ม concurrency เข้าไปบน shared-memory threads ภายหลัง แต่ Go ถูกออกแบบตั้งแต่ต้นรอบแนวคิดที่แตกต่างออกไปเรียกว่า Communicating Sequential Processes (CSP) ซึ่ง Tony Hoare เป็นผู้นิยามในปี 1978 คำพูดของ Go ที่สรุปได้ชัดที่สุดคือ:

อย่าสื่อสารโดยการแชร์หน่วยความจำ ให้แชร์หน่วยความจำโดยการสื่อสารแทน

ในทางปฏิบัติหมายความว่า:

  • Goroutines คือฟังก์ชันที่ทำงานอิสระ ใช้ทรัพยากรน้อยมาก — stack เริ่มต้นเพียงไม่กี่ KB ที่ขยายตัวได้ตามต้องการ — จึงสามารถมีได้หลายแสนตัวที่รันพร้อมกัน
  • Channels คือท่อส่งข้อมูลแบบ typed goroutines ส่งค่าเข้า channel และรับค่าจาก channel อีกตัว ทั้งการส่งข้อมูลและการ synchronize เกิดขึ้นในคำสั่งเดียว
  • Scheduler จัดสรร goroutines ลงบน OS threads (GOMAXPROCS threads ตามค่าเริ่มต้น หนึ่งตัวต่อ CPU core) คุณไม่ต้องจัดการ threads โดยตรง
บทเรียนหัวข้อ
หน้านี้ภาพรวมโมเดลและตัวอย่างแรกที่รันได้
Goroutinesgo f(), scheduler, sync.WaitGroup
Channelsสร้าง, ส่ง, รับ, unbuffered vs buffered, close, range
Selectรวม channels หลายตัว, default, timeouts
Sync Primitivessync.Mutex, sync.Once, sync/atomic
Contextcontext.Context, การยกเลิก, การส่งต่อ
PatternsWorker pool, fan-out/fan-in, pipeline

snippet ด้านล่างสร้าง goroutines 3 ตัวแบบขนาน แต่ละตัวเขียนผลลัพธ์ลงใน slice ที่จัดสรรไว้ล่วงหน้า indexed ตาม id ของตัวเอง ทำให้ผลลัพธ์เรียงลำดับเสมอ sync.WaitGroup บล็อก main จนกว่าทั้งสามตัวจะเสร็จสิ้น

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 thread2-8 KB stack vs 1-8 MBscheduler 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

💡 ตัวอย่างจากของจริง

Kubernetes ใช้ goroutine pool จัดการ controller loop ของทุก resource type พร้อมกัน

Prometheus เปิด goroutine ต่อ scrape target ทำให้ collect metrics แบบ parallel

คำพูด Go 'share memory by communicating' หมายความว่าอะไรในทางปฏิบัติ?
จำนวน OS threads เริ่มต้นที่ Go ใช้รัน goroutines คือเท่าไหร่?
ทำไม `wg.Add(1)` ถึงต้องเรียกก่อน `go func()`?