📝 Thread-safe 3 หลัก

ในทางเทคนิค โค้ดนี้ทำงานได้ถูกต้อง (Thread-safe) แต่ในเชิงปฏิบัติมี 3 เหตุผลหลัก ที่ไม่แนะนำครับ:

  1. เสี่ยงต่อการลืม Unlock (Panic): หากโค้ดระหว่าง Lock() และ Unlock() เกิด panic ขึ้นมา Mutex จะถูกล็อกค้างไว้ตลอดกาล (Deadlock) การใช้ defer mu.Unlock() ทันทีหลังล็อกจะปลอดภัยกว่า [1, 2]
  2. เขียนให้สั้นกว่าได้: คุณสามารถเขียน s.val++ ได้เลยภายใต้ Lock ไม่จำเป็นต้องประกาศตัวแปร v มาพักค่าให้วุ่นวายครับ [1]
  3. ประสิทธิภาพ (Performance): หากต้องการแค่เพิ่มค่าตัวเลข (Counter) การใช้ atomic.AddInt64 จะทำงานได้เร็วกว่าการใช้ Mutex มาก เพราะไม่ต้องหยุดการทำงานของ Thread อื่นในระดับ OS [1, 3]

ตัวอย่างที่แนะนำ:

mu.Lock()defer mu.Unlock() // ปลอดภัยกว่า
s.val++

อยากให้ช่วยดูวิธีปรับเป็นแบบ atomic เพื่อเพิ่มความเร็วด้วยไหมครับ?


การใช้ Closure ร่วมกับ Mutex มักใช้เพื่อสร้างสิ่งที่เรียกว่า "Thread-safe Wrapper" ครับ วิธีนี้จะช่วยซ่อนความซับซ้อนของการ Lock ไว้ข้างใน ทำให้คนเรียกใช้ (Caller) ไม่ต้องกังวลเรื่องการลืม Unlock
นี่คือ Pattern ที่นิยมใช้กัน:

  1. Update Pattern (Atomic-like operation)
    ใช้ Closure เพื่อ "ห่อ" ชุดคำสั่งที่ต้องการให้ทำงานแบบต่อเนื่อง (Atomic) โดยที่เราไม่ต้องสั่ง Lock/Unlock เองจากข้างนอก
func (s *State) Update(fn func(current int) int) {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    // ส่งค่าปัจจุบันเข้าไปใน Closure และรับค่าใหม่กลับมา
    s.val = fn(s.val)
}

// วิธีใช้งาน
state.Update(func(current int) int {
    return current + 1 // เขียน logic อะไรตรงนี้ก็ได้ จะคูณ จะหาร หรือเช็ค if
})
  1. Transactional Pattern
    ใช้ในกรณีที่ต้องการทำหลายอย่างพร้อมกันภายใต้ Lock เดียว เพื่อป้องกัน Data Race ระหว่างขั้นตอน
func (s *Store) WithLock(action func(s *Store)) {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    action(s)
}

// วิธีใช้งาน: ทุกอย่างในนี้จะถูก Lock ไว้ด้วยกัน
store.WithLock(func(s *Store) {
    s.val++
    s.lastUpdated = time.Now()
    fmt.Println("Updated safe!")
})

ทำไมถึงน่าใช้?

  1. Safety: ลดโอกาสเกิด Deadlock เพราะ defer mu.Unlock() ถูกจัดการเบ็ดเสร็จภายในฟังก์ชันเดียว
  2. Readability: โค้ดส่วน logic (ใน closure) แยกออกจากโค้ดส่วน synchronization (ใน wrapper) อย่างชัดเจน
  3. Consistency: มั่นใจได้ว่าทุกครั้งที่มีการเข้าถึงตัวแปร จะผ่านการ Lock เสมอเพราะเราบังคับให้ใช้ผ่านฟังก์ชันที่เตรียมไว้

ข้อควรระวัง: อย่าเรียกฟังก์ชันอื่นที่พยายามจะ Lock Mutex ตัวเดิมซ้ำซ้อนภายใน Closure เพราะจะเกิด Recursive Deadlock ได้ครับ
สนใจอยากลองดูตัวอย่างการใช้ RWMutex เพื่อแยกการอ่านและการเขียนให้เร็วขึ้นไหมครับ?


การใช้ sync.RWMutex จะช่วยให้โปรแกรมทำงานเร็วขึ้นมากในกรณีที่มีการ "อ่านเยอะ แต่เขียนน้อย" ครับ
ความต่างคือ:

ตัวอย่างการใช้ RWMutex Pattern

type Stats struct {
    mu      sync.RWMutex
    counters map[string]int
}

// 1. Write Lock: ใช้ Closure เพื่ออัปเดตค่า (ล็อกทุกคน)
func (s *Stats) Update(name string, fn func(current int) int) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.counters[name] = fn(s.counters[name])
}

// 2. Read Lock: ใช้ Closure เพื่ออ่านค่า (อ่านพร้อมกันได้ ไม่ช้า)
func (s *Stats) Snapshot(fn func(data map[string]int)) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    
    // ส่งข้อมูลออกไปให้ประมวลผล (เช่น Print หรือส่ง API) แบบ Read-only
    fn(s.counters)
}

ทำไมแบบนี้ถึง "เทพ" กว่า?

  1. High Concurrency: ถ้ามี Goroutines 100 ตัวอยากอ่านค่าพร้อมกัน ทุกตัวจะได้อ่านทันทีโดยไม่ต้องรอคิวกัน (ถ้าไม่มีคนกำลังเขียนอยู่)
  2. Safety: Snapshot ช่วยให้เรามั่นใจว่าข้อมูลที่อ่านออกมาจะไม่ถูกแก้ไขกลางคันจนเกิด Error (เช่น concurrent map iteration and map write)

ข้อควรระวัง: ภายใน RLock ห้ามเขียนข้อมูลเด็ดขาด และอย่าทำ Logic ที่ใช้เวลานานเกินไป เพราะจะไปขวางคนที่รอ Lock (Write) อยู่ครับ
อยากให้ลองทำตัวอย่าง Benchmark เปรียบเทียบความเร็วระหว่าง Mutex ปกติกับ RWMutex ดูไหมครับ?