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

Sync Primitives

Channels เหมาะสำหรับการถ่ายโอน ownership ของข้อมูลระหว่าง goroutines แต่บางครั้งคุณแค่ต้องการป้องกัน shared value ที่ goroutines หลายตัวอ่านและเขียน — cache, counter, connection pool สำหรับกรณีเหล่านี้ package sync มี primitives ระดับต่ำกว่า

sync.Mutex คือ mutual exclusion lock goroutine เดียวเท่านั้นที่ถือ lock ได้ในแต่ละเวลา ที่เหลือบล็อกบน Lock() จนกว่าผู้ถือเรียก Unlock()

var mu sync.Mutex
var count int
mu.Lock()
count++
mu.Unlock()

ใช้ defer mu.Unlock() ทันทีหลัง mu.Lock() เสมอเพื่อให้ lock ถูก release แม้ function จะ panic หรือ return ก่อนเวลา

sync.RWMutex คือ variant ที่ optimize สำหรับกรณีที่อ่านบ่อย goroutines หลายตัวสามารถถือ read lock พร้อมกันได้ด้วย RLock/RUnlock Write lock (Lock/Unlock) เป็น exclusive — รอให้ reader ทั้งหมดเสร็จก่อน แล้วบล็อก reader ใหม่จนกว่า writer เสร็จ

sync.Once รัน function ได้ครั้งเดียว ไม่ว่าจะมี goroutines กี่ตัวเรียก Do พร้อมกัน use case มาตรฐานคือ lazy, thread-safe initialization:

var once sync.Once
var instance *Config
func getConfig() *Config {
once.Do(func() {
instance = loadConfig()
})
return instance
}

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
}
สิ่งที่ได้ประโยชน์ต้นทุน
sync.Mutexง่าย, เร็วสำหรับ simple shared stategoroutine ที่ lock ต้อง unlock เสมอ
sync.RWMutexread parallel ได้RLock overhead > Mutex lock เมื่อ write บ่อย
sync.Onceinit once thread-safeไม่สามารถ reset ได้
sync/atomicเร็วที่สุดสำหรับ simple counterจำกัดแค่ primitive types
channelcomposable, saferoverhead มากกว่า 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

ความแตกต่างระหว่าง sync.Mutex และ sync.RWMutex คืออะไร?
function ที่ส่งให้ sync.Once.Do จะรันกี่ครั้ง แม้ว่า 100 goroutines จะเรียก Do?
ควรใช้ sync/atomic แทน sync.Mutex สำหรับ shared integer counter เมื่อไหร่?
`go test -race` ทำอะไร?