Sync Primitives
เมื่อ Channels ไม่เพียงพอ
หัวข้อที่มีชื่อว่า “เมื่อ Channels ไม่เพียงพอ”Channels เหมาะสำหรับการถ่ายโอน ownership ของข้อมูลระหว่าง goroutines แต่บางครั้งคุณแค่ต้องการป้องกัน shared value ที่ goroutines หลายตัวอ่านและเขียน — cache, counter, connection pool สำหรับกรณีเหล่านี้ package sync มี primitives ระดับต่ำกว่า
sync.Mutex
หัวข้อที่มีชื่อว่า “sync.Mutex”sync.Mutex คือ mutual exclusion lock goroutine เดียวเท่านั้นที่ถือ lock ได้ในแต่ละเวลา ที่เหลือบล็อกบน Lock() จนกว่าผู้ถือเรียก Unlock()
var mu sync.Mutexvar count int
mu.Lock()count++mu.Unlock()ใช้ defer mu.Unlock() ทันทีหลัง mu.Lock() เสมอเพื่อให้ lock ถูก release แม้ function จะ panic หรือ return ก่อนเวลา
sync.RWMutex
หัวข้อที่มีชื่อว่า “sync.RWMutex”sync.RWMutex คือ variant ที่ optimize สำหรับกรณีที่อ่านบ่อย goroutines หลายตัวสามารถถือ read lock พร้อมกันได้ด้วย RLock/RUnlock Write lock (Lock/Unlock) เป็น exclusive — รอให้ reader ทั้งหมดเสร็จก่อน แล้วบล็อก reader ใหม่จนกว่า writer เสร็จ
sync.Once
หัวข้อที่มีชื่อว่า “sync.Once”sync.Once รัน function ได้ครั้งเดียว ไม่ว่าจะมี goroutines กี่ตัวเรียก Do พร้อมกัน use case มาตรฐานคือ lazy, thread-safe initialization:
var once sync.Oncevar instance *Config
func getConfig() *Config { once.Do(func() { instance = loadConfig() }) return instance}sync/atomic
หัวข้อที่มีชื่อว่า “sync/atomic”Package sync/atomic มี lock-free operations บน integer และ pointer types โดยใช้ CPU hardware instructions สำหรับ counter หรือ flag ง่าย ๆ atomics เร็วกว่า mutex เพราะไม่มี lock contention
package main
import ( "fmt" "sync" "sync/atomic")
type SafeCounter struct { mu sync.Mutex v int}
func (c *SafeCounter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.v++}
func (c *SafeCounter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.v}
func main() { // sync.Mutex: 100 goroutines increment ทีละ 1 -> 100 var wg sync.WaitGroup c := &SafeCounter{} for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() c.Inc() }() } wg.Wait() fmt.Println("counter:", c.Value()) // 100
// sync.Once: function รันครั้งเดียวแม้เรียก 3 ครั้ง var once sync.Once for i := 0; i < 3; i++ { once.Do(func() { fmt.Println("only once") }) }
// sync/atomic: 50 goroutines add 1 -> 50 var atomicCount int64 for i := 0; i < 50; i++ { wg.Add(1) go func() { defer wg.Done() atomic.AddInt64(&atomicCount, 1) }() } wg.Wait() fmt.Println("atomic count:", atomic.LoadInt64(&atomicCount)) // 50}Loading Go runtime (first run only, ~8 MB)…
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| sync.Mutex | ง่าย, เร็วสำหรับ simple shared state | goroutine ที่ lock ต้อง unlock เสมอ |
| sync.RWMutex | read parallel ได้ | RLock overhead > Mutex lock เมื่อ write บ่อย |
| sync.Once | init once thread-safe | ไม่สามารถ reset ได้ |
| sync/atomic | เร็วที่สุดสำหรับ simple counter | จำกัดแค่ primitive types |
| channel | composable, safer | overhead มากกว่า mutex |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- mutex ช้ากว่า channel เสมอ — mutex เร็วกว่า channel สำหรับ protect simple counter
- sync.Map แทน map+mutex ได้เสมอ — sync.Map เหมาะกับ read-heavy, append-only ไม่ใช่ general map
- atomic.AddInt64 ใช้งานได้โดยไม่มีข้อกังวล — ต้องระวัง 64-bit alignment บน 32-bit platform
- defer mu.Unlock() ช้ากว่า explicit unlock — defer มี overhead น้อยมาก ใช้เพื่อ safety เสมอ
💡 ตัวอย่างจากของจริง
Kubernetes ใช้ sync.RWMutex protect in-memory object cache — หลาย reader, writer น้อย เหมาะกับ RWMutex
Prometheus ใช้ atomic counter สำหรับ metric increment — lock-free, ไม่เสีย throughput
etcd ใช้ sync.Once สำหรับ singleton initialization ของ raft state machine