บทที่ 16 · Part 5 — Concurrency That Stops

The Go Memory Model

เข้าใจ happens-before, data race และเหตุผลที่โค้ดซึ่งดู atomic อาจยังไม่ถูกต้อง

โค้ดที่ “ลองรันแล้วได้ค่าเดิม” ไม่ได้แปลว่าปลอดภัยเมื่อมีหลาย goroutine Compiler, CPU และ runtime มีสิทธิ์ reorder การอ่านเขียนตราบใดที่ยังรักษา semantics ของ goroutine เดียว ถ้าไม่มี synchronization เราไม่สามารถใช้เวลาในหัวว่า “บรรทัดนี้น่าจะรันก่อน” เป็นหลักฐานได้

จบบทนี้คุณจะ

  • อธิบาย data race และ happens-before โดยไม่พึ่ง timing
  • รู้ว่า channel, mutex และ atomic สร้าง synchronization อย่างไร
  • ใช้ race detector เป็นหลักฐานหนึ่ง แต่ไม่เข้าใจผิดว่าพิสูจน์ทุก execution แล้ว

Data Race ไม่ใช่แค่ค่าคลาดเคลื่อน

data race เกิดเมื่อ goroutine 2 ตัว access memory location เดียวกันพร้อมกัน มีอย่างน้อย 1 write และไม่มี synchronization ที่เหมาะสม ตัวอย่างนี้ผิดแม้ ready มักกลายเป็น true หลัง assignment:

var value string
var ready bool

go func() {
    value = "settled"
    ready = true
}()

for !ready {
}
fmt.Println(value)

เราไม่มี happens-before edge ที่รับรองว่า goroutine หลักจะเห็น ready หรือจะเห็น value หลังจากนั้น การใส่ time.Sleep เพียงเปลี่ยนโอกาส ไม่ได้สร้าง synchronization และอาจทำ test ผ่านบน laptop แต่ fail เมื่อ CPU/load เปลี่ยน

Go Memory Model แนะนำอย่างตรงไปตรงมาว่าโปรแกรมที่แก้ข้อมูลร่วมกัน ต้อง serialize access ด้วย channel operation หรือ synchronization primitives ใน sync/sync/atomic โปรแกรมที่ไม่มี data race จะให้ผลแบบ sequentially consistent ตามข้อกำหนดที่เอกสารอธิบาย

Happens-Before คือหลักฐานของ Visibility

channel send ที่เสร็จก่อน receive ที่สอดคล้องกันสร้าง ordering; unlock บน mutex เกิดก่อน lock ครั้งถัดไป ที่สำเร็จ; atomic operations มี ordering ตาม contract ของ package การพูดว่า “channel ส่งข้อมูล” จึงมีทั้ง การสื่อสารและ memory visibility ไม่ใช่แค่ queue

แก้ตัวอย่างด้วย ownership transfer จะชัดที่สุด:

result := make(chan string, 1)
go func() {
    result <- "settled"
}()

value := <-result
fmt.Println(value)

หรือถ้าหลาย method ต้องอ่าน/แก้ state ร่วมกัน ให้รวม invariant ไว้หลัง mutex เดียว:

type State struct {
    mu     sync.RWMutex
    status string
}

func (s *State) Set(status string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.status = status
}

mutex ปกป้อง invariant ไม่ใช่เพียง field แยกตัว ถ้า status กับ updatedAt ต้องเปลี่ยนพร้อมกัน ให้แก้ทั้งคู่ใน critical section เดียว การใช้ atomic แยก 2 ตัวอาจไม่มี race แต่ผู้อ่านเห็นคู่สถานะที่ ไม่เคยมีจริงได้ Race freedom จึงยังไม่เท่ากับ correctness

Atomic ใช้กับ State ที่เล็กและเป็นอิสระ

typed atomics เช่น atomic.Int64 และ atomic.Bool เหมาะกับ counter/flag ที่ semantics เป็น operation เดียว เช่นจำนวน in-flight jobs แต่ไม่ควรประกอบ state machine หลาย fieldด้วย atomic เพราะ reasoning ยากและ ABA/lost transition อาจเกิด ใช้ atomic.Pointer[T] สำหรับ immutable snapshot ได้เมื่อ update ทั้งก้อนและมีหลักฐานว่าความซับซ้อนคุ้มกว่า mutex

double-checked locking ที่อ่าน pointer โดยไม่ synchronize ก่อน lock ยังเป็น race ให้ใช้ sync.Once, sync.OnceValue หรือ lock ครบทุก access ตาม contract

Race Detector ช่วยหา Execution ที่เกิดขึ้น

go test -race ./...
go test -race -count=20 ./internal/worker

race detector instrument memory access และรายงาน race ที่เกิดใน execution ที่รัน มันมี overhead และไม่พิสูจน์ path ที่ test ไม่ถึง จึงต้องใช้ร่วมกับ behavior tests, stress scenarios และ review ของ ownership ทุก report ต้องแก้ อย่า suppress เพราะ “ยังไม่เคยพัง production”

Production Toolbox

Default: ไม่แชร์ mutable state ถ้าไม่จำเป็น; ใช้ channel เมื่อถ่าย ownership/work, mutex เมื่อปกป้อง invariant และ typed atomic สำหรับค่าขนาดเล็กอิสระ เปิด -race ใน CI job ที่เหมาะกับ platform ใช้ sync.Map เฉพาะ workload ที่ตรงกับ contract ของมัน ไม่ใช่แทน map+mutex ทุกแห่ง

Checklist ของ Shared State

  • ทุก shared write มี synchronization edge ที่อธิบายได้
  • mutex ปกป้อง invariant ครบ ไม่แยก field ที่ต้องสอดคล้องกัน
  • atomic state เป็น operation เดียวและมี semantics ชัด
  • ไม่มี timing/sleep ถูกใช้เป็น synchronization
  • race detector รัน path ที่มี concurrency จริงและ report เป็น 0
  • API ระบุว่า type/method safe สำหรับ concurrent use หรือไม่

อ่านเพิ่ม: The Go Memory Model, Data Race Detector และ Mutex or Channel?