บทเรียน 26 · Day 6 · Concurrency

Worker Pool: จำกัดจำนวนคนทำงาน

~15 นาที · ต้องมี: บทเรียน 21, บทเรียน 22, บทเรียน 24

goroutine ถูกมาก — สร้างแสนตัวยังไหว แต่ถ้าแต่ละตัวเปิด connection ไป database ที่รับได้ 100 connection คุณไม่ได้ทำให้ระบบเร็วขึ้น คุณทำให้ database ล่ม
Worker Pool คือรูปแบบที่บอกว่า "มีงานล้านชิ้นก็ได้ แต่ทำพร้อมกันแค่ N" — และเป็นบทที่เอาทุกอย่างของ Day 6 มารวมกัน

1 · โครงสร้าง: สองท่อกับกลุ่มคนทำงาน

                   ┌─────────────────┐
   main ──ส่งงาน──►│  jobs channel   │
                   └────────┬────────┘
                            │  worker แย่งกันดึงงาน
              ┌─────────────┼─────────────┐
              ▼             ▼             ▼
          worker 1      worker 2      worker 3     ← มีเท่านี้ ไม่เพิ่ม
              │             │             │
              └─────────────┼─────────────┘
                            ▼
                   ┌─────────────────┐
                   │ results channel │──► main รวมผล
                   └─────────────────┘

   งาน 9 ชิ้น · worker 3 ตัว  →  แต่ละตัวทำ 3 รอบ
🏦 เทียบให้เห็นภาพ — ธนาคารสาขาเดียวที่เปิด 3 เคาน์เตอร์

เช้าวันจันทร์คนมาต่อคิว 200 คน แต่ธนาคารไม่ได้เปิดเคาน์เตอร์ 200 ช่อง
· เครื่องกดบัตรคิว = jobs — งานทุกชิ้นกองรวมไว้ที่เดียว แต่กระดาษในเครื่องมีจำกัดเท่าจำนวนงาน (นั่นคือ buffer ของ channel) ถ้ายัดเกินนั้น คนแจกบัตรเองนั่นแหละที่ต้องยืนรอจนกว่าจะมีคิวถูกเรียกไป
· เคาน์เตอร์ 3 ช่อง = worker 3 ตัว — ช่องไหนว่างก็กดเรียกคิวถัดไปเอง ไม่มีใครคอยสั่ง
· ตะกร้าใบเสร็จ = results — ที่รวมผลงานของทุกช่อง
บัตรคิวหนึ่งใบมีคนเรียกได้ช่องเดียว เหมือนงานหนึ่งชิ้นที่ worker ดึงไปได้แค่ตัวเดียว

ที่ไม่เหมือนกัน: ธนาคารมีเวลาปิดสาขา พนักงานเลิกงานเองได้ แต่ worker ไม่มีเวลาเลิกงาน — มันจะยืนรอที่ jobs ไปเรื่อย ๆ จนกว่าจะมีคนสั่ง close(jobs) บอกว่า "หมดงานแล้ว" (หัวข้อ 3 ขั้นที่ 2)

2 · ตัว worker

func worker(
    id int,
    jobs <-chan int,        // รับอย่างเดียว
    results chan<- int,     // ส่งอย่างเดียว
    wg *sync.WaitGroup,     // pointer!
) {
    defer wg.Done()

    for job := range jobs {     // วนจนกว่า jobs จะถูกปิด
        time.Sleep(200 * time.Millisecond)   // สมมติว่างานหนึ่งชิ้นใช้ 200ms
        results <- job * 2
    }

    fmt.Printf("Worker %d: ไม่มีงานแล้ว จบ\n", id)
}

ทั้งหมดนี้คือของที่คุณเรียนมาแล้ว ประกอบกัน:
• <-chan / chan<- — directional channel (บทเรียน 22)
• for range jobs — วนจนถูก close (บทเรียน 22)
• *sync.WaitGroup — ต้องเป็น pointer (บทเรียน 21)
• defer wg.Done() — บรรทัดแรกเสมอ (บทเรียน 08)
ไม่มีของใหม่เลยสักอย่าง — worker pool คือ "วิธีเรียงของเก่า"

🗣 ศัพท์ที่เพิ่งเรียนไป
worker pool
“กลุ่มคนทำงานจำนวนคงที่” ที่แย่งกันดึงงานจากกองเดียวกัน · พูดว่า “ตรงนี้ผมใช้ worker pool ขนาด 3 ครับ” จะสื่อกว่า “ผมเปิด goroutine 3 ตัว”
fan-out / fan-in
กระจายงานจากท่อเดียวออกไปหลาย goroutine (fan-out) แล้วรวมผลกลับมาท่อเดียว (fan-in) · ถามได้เลยว่า “อันนี้คือ pattern fan-out/fan-in ใช่ไหม”
ในบทนี้ fan-in ทำแบบง่ายที่สุดคือให้ทุก worker เขียนลง results ตัวเดียวกัน · ส่วนอีกท่าที่เจอบ่อยกว่า (และเป็นนิยามที่ Go blog เรื่อง pipelines ใช้) คือ merge หลาย channel ให้เหลือท่อเดียว — ถ้าคุยกับ senior แล้วเขางง ให้บอกไปว่าหมายถึงท่าไหน
directional channel
<-chan รับได้อย่างเดียว · chan<- ส่งได้อย่างเดียว · ประโยชน์คือเขียนผิดทางแล้ว compile ไม่ผ่านตั้งแต่แรก ไม่ต้องรอไปเจอตอนรัน

3 · ตัวประกอบร่าง

func main() {
    const numJobs, numWorkers = 9, 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)
    var wg sync.WaitGroup

    // 1) เปิด worker pool — จำนวนคงที่
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // 2) ส่งงานทั้งหมด แล้วปิดท่อ
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)              // บอกว่าไม่มีงานเพิ่มแล้ว

    // 3) รอ worker ครบแล้วปิด results — ต้องอยู่ใน goroutine
    go func() {
        wg.Wait()
        close(results)
    }()

    // 4) รวมผล — จบเองเมื่อ results ถูกปิด
    sum := 0
    for r := range results {
        sum += r
    }
    fmt.Println("ผลรวมทั้งหมด:", sum)
}

ผมรันแล้วได้:

Worker 2: ไม่มีงานแล้ว จบ
Worker 1: ไม่มีงานแล้ว จบ
Worker 3: ไม่มีงานแล้ว จบ
ผลรวมทั้งหมด: 90
ใช้เวลา: 600ms (ถ้าทำทีละงานจะใช้ 1.8s)

9 งาน × 200ms = 1.8 วินาทีถ้าทำทีละอัน · worker 3 ตัวทำได้ใน 600ms เร็วขึ้น 3 เท่าพอดี = จำนวน worker

4 · ขั้นตอนที่ 3 คือขั้นที่คนพลาดมากที่สุด

results := make(chan int)      // ← unbuffered

// ... เปิด worker · ส่งงาน · close(jobs) เหมือนเดิม ...

// เอา goroutine ครอบออก เขียนตรง ๆ แบบนี้แทน
wg.Wait()
close(results)

sum := 0
for r := range results {
    sum += r
}

อ่านดูแล้วตรงไปตรงมากว่าเดิม — ทำไมถึงใช้ไม่ได้?

ผมรันแล้วได้:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [sync.WaitGroup.Wait]:

ทุกคนรอกันเองครบวง:
• main ค้างที่ wg.Wait() รอ worker จบ
• worker ค้างที่ results <- ... รอคนมารับ
• คนที่จะมารับคือ for range results ซึ่งอยู่ใต้ wg.Wait() — ไปไม่ถึงสักที

กฎที่ถอดออกมาได้: คนที่ รอ กับคนที่ รับ ต้องไม่ใช่ goroutine เดียวกัน
สำนวนมาตรฐานจึงเป็น go func(){ wg.Wait(); close(results) }() — ให้การรอไปเกิดข้างหลัง ส่วน main ว่างมารับผลได้เรื่อย ๆ
และนี่คือคำตอบของคำถามค้างในบทเรียน 22: เมื่อมีคนส่งหลายคน ให้ตัวที่รอทุกคนเสร็จเป็นคนปิด

⚠️ กับดักซ้อน — ตัวอย่างในสไลด์รอดมาได้เพราะบังเอิญ
ผมลองเปลี่ยน results เป็น make(chan int, numJobs) (buffer ใหญ่เท่าจำนวนงานพอดี ตามสไลด์) แล้วเขียน wg.Wait() ตรง ๆ แบบนี้ — มันทำงานได้ พิมพ์ ผลรวม: 90 ปกติ
เพราะ worker ยัดผลลงบัฟเฟอร์ได้ครบโดยไม่ต้องรอใคร
แต่พอมีงานเยอะกว่า buffer แม้แค่ชิ้นเดียว มันจะกลับมาค้างทันที — นี่คือบั๊กที่ผ่านเทสตอน dev (ข้อมูลน้อย) แล้วไปล่มตอน production
เขียนสำนวนที่ถูกไว้ตั้งแต่แรก ดีกว่าเขียนแบบที่บังเอิญไม่พัง

📦 เทียบให้เห็นภาพ — คนส่งของที่ต้องเซ็นรับมือต่อมือ

สั่งของ 9 กล่อง มีคนส่ง 3 คนทยอยมาถึงหน้าบ้าน กฎของร้านคือ ต้องเซ็นรับมือต่อมือ วางทิ้งไว้หน้าบ้านไม่ได้ แต่คุณยืนอยู่ในบ้านแล้วประกาศว่า “ขอรอให้ทุกคนส่งเสร็จก่อน แล้วค่อยเปิดประตู”
คนส่งยืนถือกล่องรอคุณเปิดประตู ส่วนคุณรอให้เขาส่งเสร็จก่อนถึงจะเปิด ไม่มีใครขยับได้เลย — นี่คือสิ่งที่เกิดขึ้นเมื่อ wg.Wait() อยู่ก่อน for range results
ทางแก้คือให้คนอื่นไปยืนนับแทน นั่นคือ go func(){ wg.Wait(); close(results) }() ส่วนตัวคุณเปิดประตูรับของไปเรื่อย ๆ

ที่ไม่เหมือนกัน: ชีวิตจริงถ้ามี “ตู้รับพัสดุ” หน้าบ้าน คนส่งก็หย่อนไว้แล้วเดินกลับได้เลย — ตู้ใบนั้นคือ buffer ของ channel และเป็นเหตุผลที่เวอร์ชันผิดในกล่องเฉลยข้างบน (buffer เท่าจำนวนงาน + wg.Wait() ตรง ๆ) “บังเอิญ” ไม่ค้าง
แต่ตู้มีขนาดจำกัด พอของล้นตู้แม้แค่กล่องเดียว ทุกอย่างกลับไปค้างเหมือนเดิม
ส่วนโค้ดหลักในหัวข้อ 3 ไม่ได้พึ่งตู้ใบนี้เลย — ผมลองเปลี่ยน results เป็น make(chan int) (ไม่มีตู้) โดยไม่แตะอย่างอื่น มันก็ยังได้ 90 ใน 600ms เท่าเดิม เพราะมีคนคอยเปิดประตูรับอยู่ตลอด

🗣 ศัพท์ที่เพิ่งเรียนไป
deadlock
สภาพที่ทุกฝ่าย “รอกันเองครบวง” จนไม่มีใครไปต่อได้ · เวลาแจ้ง senior ให้พูดว่า “มัน deadlock ตรง…” พร้อมส่งคำในวงเล็บเหลี่ยม เช่น [sync.WaitGroup.Wait] ไปด้วย เพราะนั่นคือคำที่บอกว่าค้างอยู่ตรงไหน
block / blocking
“ค้างรออยู่ตรงนั้น ไปบรรทัดถัดไปไม่ได้” · results <- x ที่ยังไม่มีคนรับ เรียกว่าบล็อกอยู่ · ถามสั้น ๆ ได้ว่า “บรรทัดนี้ block ไหม”
unbuffered / buffered channel
ไม่มีตู้รับของ ต้องส่งมือต่อมือ / มีตู้รับของกี่ช่องก็หย่อนไว้ได้เท่านั้น · คำถามที่สื่อที่สุดเวลาไล่บั๊กแบบนี้คือ “channel ตัวนี้ buffered หรือ unbuffered”
idiomatic (ตามสำนวนของภาษา)
ถ้าได้ยินว่า “โค้ดนี้ไม่ idiomatic” แปลว่า “ทำงานได้อยู่ แต่ไม่ใช่ท่าที่คนอื่นเขาเขียนกัน” · go func(){ wg.Wait(); close(ch) }() คือท่ามาตรฐานของ Go

5 · จะตั้งจำนวน worker เท่าไหร่ดี?

คำตอบขึ้นกับว่างานของคุณ รออะไรอยู่ หรือ คิดอะไรอยู่
ผมวัดจริงด้วยงาน 60 ชิ้น บนเครื่องที่มี GOMAXPROCS = 10:

งานที่รอ I/O (Sleep 100ms)

worker   1 → 6.09s
worker   2 → 3.05s
worker   5 → 1.22s
worker  10 → 610ms
worker  20 → 310ms
worker  60 → 100ms
worker 200 → 100ms   ← ตันเพราะงานหมด

ดีขึ้นเรื่อย ๆ จนกว่า worker จะเท่าจำนวนงาน เพราะระหว่างรอ I/O ไม่ได้ใช้ CPU เลย

งานที่กิน CPU (คำนวณล้วน)

worker   1 → 10.16s
worker   2 →  5.38s
worker   5 →  2.57s
worker  10 →  1.74s  ← = GOMAXPROCS
worker  20 →  1.71s
worker  60 →  1.60s

ตันที่ 10 = จำนวน core เพิ่ม worker ต่อไม่ช่วยอะไร เพราะไม่มี CPU ให้ใช้แล้ว

🧺 เทียบให้เห็นภาพ — ซักผ้ากับรีดผ้า

งานบ้านวันอาทิตย์มีสองแบบที่คนละเรื่องกันเลย
· ซักผ้า — ยัดเครื่องแล้วรอ 40 นาที ระหว่างรอไม่ได้ออกแรงอะไรเลย มีเครื่อง 5 เครื่องก็เดินไปยัดพร้อมกันได้สบาย เหนื่อยเท่าเดิม
· รีดผ้า — ต้องมีคนยืนรีดตลอดเวลา บ้านนี้มีโต๊ะรีดกับเตารีด 10 ชุด เรียกคนมาช่วยได้ถึง 10 คน เพิ่มคนที่เท่าไหร่ก็เร็วขึ้นจริงทุกคน แต่คนที่ 11 ไม่มีเตารีดให้จับ ได้แต่ยืนรอสลับกับคนอื่น
ตรงกับตัวเลขสองฝั่งข้างบนพอดี: ฝั่งซ้ายดีขึ้นเรื่อย ๆ ถึง 60 ตัว ส่วนฝั่งขวาดีขึ้นเรื่อย ๆ จนถึง 10 แล้วแทบไม่ขยับอีก — จำนวนเตารีด = จำนวน core

ที่ไม่เหมือนกัน: คนที่ 11 ในบ้านจริงคือยืนเฉย ๆ แต่ goroutine ตัวที่ 11 ไม่ได้ถูกสั่งให้หยุด — Go จะสลับให้มันได้จับเตารีดเป็นช่วง ๆ เอง งานเลยไม่เสร็จเร็วขึ้น แต่ก็ไม่ได้ค้าง (เสียเวลาสลับเพิ่มนิดหน่อยด้วยซ้ำ)
และเครื่องซักผ้าที่บ้านเป็นของเรา จะเปิดพร้อมกันกี่เครื่องก็เรื่องของเรา แต่ปลายทางของงาน I/O เป็นของคนอื่น — API และ database มีเพดานว่ารับได้กี่คนพร้อมกัน ยิงเกินเพดานไม่ได้แปลว่าเร็วขึ้น แต่แปลว่าโดนปฏิเสธ หรือทำระบบเขาล่ม

📊 กฎการตั้งจำนวน worker

ก่อนอ่านแถวสุดท้าย — connection คือ "สายเชื่อม" หนึ่งเส้นที่โปรแกรมเปิดค้างไว้กับ database เพื่อส่งคำสั่ง SQL การเปิดใหม่ทุกครั้งช้ามาก โปรแกรมจึงเปิดไว้ล่วงหน้าจำนวนหนึ่งแล้วหมุนเวียนใช้
กองสายที่เตรียมไว้นั้นเรียกว่า connection pool และมีเพดานที่เราตั้งเองได้ (ใน Go คือ db.SetMaxOpenConns(25)) — บทเรียน 27 จะได้ต่อ database จริง แล้วเห็นตัวเลขนี้ด้วยตาตัวเอง

งานแบบไหนตั้งเท่าไหร่ตัวจำกัดจริง ๆ คือ
CPU-bound — คำนวณ, เข้ารหัส, ประมวลผลภาพruntime.GOMAXPROCS(0)จำนวน CPU core
I/O-bound — เรียก API, อ่านไฟล์สูงได้ (50–500)bandwidth · rate limit ปลายทาง
Database≤ ขนาด connection poolจำนวน connection ที่ DB รับได้

ข้อสุดท้ายสำคัญที่สุดในงานจริง — worker 500 ตัวยิงเข้า pool ที่มี 25 connection = 475 ตัวนั่งรอคิว เพิ่มแต่ความซับซ้อน ไม่ได้เร็วขึ้น
หาให้เจอก่อนว่าคอขวดอยู่ตรงไหน แล้วค่อยตั้งตัวเลข — ไม่ใช่เดาแล้วหวังว่าเยอะไว้ก่อนดีกว่า

🗣 ศัพท์ที่เพิ่งเรียนไป
I/O-bound / CPU-bound
เวลาส่วนใหญ่หมดไปกับ “การรอ” ของนอกเครื่อง (เน็ต ดิสก์ database) / หมดไปกับ “การคำนวณ” · ประโยคที่เอาไปพูดได้เลย: “งานนี้เป็น I/O-bound เลยตั้ง worker เกินจำนวน core ได้”
bottleneck (คอขวด)
จุดที่ช้าที่สุด ซึ่งกำหนดความเร็วของทั้งเส้นทาง · ถามว่า “คอขวดของงานนี้อยู่ที่ CPU, เน็ต หรือ database” จะได้คำตอบตรงกว่า “ทำยังไงให้เร็วขึ้น” ลอย ๆ
connection pool
กองสายเชื่อม database ที่เปิดค้างไว้แล้วหมุนเวียนใช้ · ก่อนตั้งจำนวน worker ให้ถามก่อนเสมอว่า “pool size เท่าไหร่”
rate limit
เพดานที่ปลายทางกำหนดว่ายิงได้กี่ครั้งต่อช่วงเวลา · เกินแล้วเขาจะตอบ 429 Too Many Requests กลับมาแทน

6 · GOMAXPROCS — เพดานที่แท้จริง

runtime.GOMAXPROCS(0)   // ถามค่าปัจจุบัน (0 = ไม่เปลี่ยน)
runtime.NumCPU()         // จำนวน core ที่มองเห็น

บนเครื่องที่ผมทดสอบ ทั้งสองค่าเป็น 10 เท่ากัน

GOMAXPROCS คือจำนวน goroutine ที่ รันบน CPU จริง ๆ พร้อมกัน ได้สูงสุด ณ ขณะหนึ่ง — คือจำนวน P ใน GMP model (บทเรียน 21)
goroutine อีกหมื่นตัวไม่ได้หายไปไหน — แค่เข้าคิวรอ P ว่าง

สำคัญเวลารันใน container: Go เวอร์ชันใหม่อ่าน CPU limit ของ cgroup (กติกาของ Linux ที่ใช้จำกัดว่าคอนเทนเนอร์ตัวหนึ่งใช้ CPU ได้เท่าไหร่) มาตั้ง GOMAXPROCS ให้เอง แต่ถ้าใช้ Go เก่า (ก่อน 1.25) มันจะเห็น core ของ เครื่องแม่ เช่นเห็น 64 core ทั้งที่ container จำกัดไว้ 2 → สร้าง P เกินความจำเป็น → เสียเวลาสลับงานไปมาโดยเปล่าประโยชน์
ถ้า service ใน Kubernetes ช้าผิดปกติ นี่คือจุดแรกที่ควรเช็ค

🗣 ศัพท์ที่เพิ่งเรียนไป
GOMAXPROCS / P (Processor)
P คือ “สิทธิ์ในการรัน” หนึ่งใบ · GOMAXPROCS คือจำนวนใบทั้งหมด ที่เหลือเข้าคิวรอ · ประโยคที่ควรใช้คือ “goroutine สร้างเยอะได้ แต่ที่ executing โค้ด Go จริงพร้อมกันไม่เกิน GOMAXPROCS” · คำว่า “โค้ด Go” สำคัญ — ตัวที่ค้างรอ syscall หรือรอเน็ตอยู่ ไม่นับ (มันคืน P ให้ตัวอื่นใช้ไปแล้ว) นี่คือเหตุผลที่ฝั่ง I/O ในหัวข้อ 5 ตั้ง worker 60 บนเครื่อง 10 core แล้วยังเร็วขึ้นได้
concurrency vs parallelism
concurrency = จัดการหลายงานสลับกันไปมาได้ (goroutine หมื่นตัวก็ยังไหว) · parallelism = รันพร้อมกันจริงบน core คนละตัว (จำกัดที่ GOMAXPROCS) · สองคำนี้คนใช้สลับกันบ่อย ถ้าแยกได้จะคุยกับ senior รู้เรื่องขึ้นมาก · เวลา service ใน container ช้าผิดปกติ ให้ถามว่า “GOMAXPROCS อ่านจาก CPU limit หรืออ่านจาก core ของเครื่องแม่”

ทดสอบความเข้าใจ

1. ทำไม close(results) ต้องอยู่ใน go func(){ wg.Wait(); ... }()?

ถ้า main ค้างที่ wg.Wait() ก็ไปไม่ถึง for range results — แต่ worker ค้างอยู่ที่ results <- ... รอคนรับ ทุกคนรอกันเอง → deadlock — main ค้างที่ wg.Wait() จึงขึ้นว่า goroutine 1 [sync.WaitGroup.Wait] ตรงกับผลรันจริงในหัวข้อ 4
ย้ายการรอไปไว้ข้างหลัง main จึงว่างมารับผลได้
ทำไมข้ออื่นผิด: close เรียกจากที่ไหนก็ได้ ไม่ได้จำกัดว่าต้องเป็น goroutine · wg.Wait() เรียกใน main ได้ปกติ ปัญหาคือเรียกก่อนมารับผลต่างหาก · และไม่มีกฎเรื่อง "ต้องปิดหลัง close(jobs) กี่รอบ" เลย ลำดับที่สำคัญคือ ใครรอ ไม่ใช่ เมื่อไหร่

2. งานเรียก API ภายนอก 1000 ครั้ง ควรตั้ง worker กี่ตัว?

งาน I/O ไม่ได้ใช้ CPU ระหว่างรอ → ตั้งเกินจำนวน core ได้สบาย (ผมวัดได้ว่า worker 60 ตัวเร็วกว่า 10 ตัวถึง 6 เท่าในงานแบบนี้)
แต่ตัวจำกัดจริงคือปลายทาง — ยิง 1000 พร้อมกันมักโดน 429 Too Many Requests หรือทำ API เขาล่ม
ถ้าเป็น database ให้ยึดขนาด connection pool เป็นเพดาน

3. งานคำนวณล้วน 60 ชิ้น บนเครื่อง 10 core — เพิ่ม worker จาก 10 เป็น 60 ได้ผลยังไง?

ผมวัดจริง: worker 10 → 1.74s · 20 → 1.71s · 60 → 1.60s — เกือบเท่ากันหมด
งาน CPU-bound ตันที่ GOMAXPROCS เพราะไม่มี core ว่างให้ใช้แล้ว goroutine ที่เกินแค่เข้าคิวรอ
เทียบกับงาน I/O ที่ 10 → 610ms แต่ 60 → 100ms (เร็วขึ้น 6 เท่า) ประเภทของงานกำหนดคำตอบ

4. GOMAXPROCS ใน GMP model คืออะไร?

คือจำนวน P (Processor) — สิทธิ์ในการรันโค้ด Go goroutine จะสร้างกี่หมื่นตัวก็ได้ แต่ที่ executing จริงพร้อมกัน ไม่เกินค่านี้
ระวังตอนรันใน container: Go เก่า (ก่อน 1.25) เห็น core ของเครื่องแม่ ไม่ใช่ CPU limit ที่ตั้งไว้ — ทำให้สร้าง P เกินจำเป็น

🛠️ ภารกิจ — Pipeline ร้านอาหาร (Workshop จากสไลด์)

นี่คือ workshop ท้ายสไลด์ Day 6 — ทำเองก่อน ค่อยเทียบเฉลย
เป้าหมาย output:

✅ Burger ready
✅ Pizza ready
✅ Bubble Tea ready

All orders completed 🎉

ลำดับของสามบรรทัดแรกสลับได้ทุกครั้งที่รัน เพราะ worker 2 ตัว แย่งงานกัน — เห็น Pizza ขึ้นก่อน Burger ไม่ได้ผิด
เกณฑ์ว่าถูกคือ ครบ 3 บรรทัด ไม่ซ้ำ ไม่ขาด และ All orders completed 🎉 ต้องอยู่ท้ายสุดเสมอ

  1. สร้าง type Order struct { ID int; Menu string } และ type Result struct { Order Order; Ready bool }
  2. Producer: ส่ง 3 ออเดอร์ (Burger, Pizza, Bubble Tea) เข้า orders channel แล้วปิด
  3. Worker: เปิด 2 ตัว ดึงจาก orders หน่วงเวลา 300ms แล้วส่งเข้า results
  4. Main: ปิด results ให้ถูกวิธี แล้ววนพิมพ์ ✅ ... ready · ปิดท้ายด้วย All orders completed 🎉
  5. ใช้ directional channel ที่ทุก parameter
  6. รัน go vet ./... และ go run -race . — ต้องเงียบทั้งคู่
  7. ท้าทาย 1: ตั้ง results := make(chan Result) แบบไม่มี buffer ก่อน แล้วค่อยเอา go func(){ wg.Wait(); close(results) }() ออก เรียก wg.Wait() ตรง ๆ แทน → โปรแกรมต้องค้าง จดคำในวงเล็บเหลี่ยมมา
    จากนั้นเปลี่ยนเป็น make(chan Result, 3) โดยไม่แก้อย่างอื่น แล้วรันใหม่ — คราวนี้มันไม่ค้าง ทำไม? (ใบ้: กับดักซ้อนในหัวข้อ 4)
  8. ท้าทาย 2 — นับให้ปลอดภัย: เพิ่มตัวนับ completed ที่ทุก worker เพิ่มค่าเอง
    ทำสองเวอร์ชัน: แบบไม่ล็อก กับแบบใส่ sync.Mutex
    รัน -race ทั้งสองแบบ แล้วเทียบผล (ต่อจากบทเรียน 24)
  9. ท้าทาย 3 — ใส่ context: เพิ่ม ctx context.Context เป็น parameter ตัวแรกของ worker แล้วให้มันหยุดกลางคันได้ด้วย select
    ตั้ง WithTimeout 400ms — เสร็จกี่ออเดอร์? (ต่อจากบทเรียน 25)
  10. ท้าทาย 4 — วัดจุดอิ่มตัวเอง: เพิ่มเป็น 60 ออเดอร์ แล้วลอง worker = 1, 2, 5, 10, 20, 60 จับเวลาทุกครั้ง
    จากนั้นเปลี่ยน "หน่วงเวลา" เป็น "วน for คำนวณเลข 200 ล้านรอบ" แล้ววัดใหม่ — กราฟต่างกันตรงไหน?

💬 ท้าทาย 4 คือข้อที่ทำให้คุณเลิกเดาตัวเลข worker ไปตลอดชีวิต — ส่งตัวเลขที่วัดได้มาเทียบกับของผมได้เลย (เครื่องคนละรุ่นจะได้ไม่เท่ากัน แต่รูปร่างของกราฟควรเหมือนกัน)
จบ concurrency แล้ว — บทสุดท้ายของ Day 6 คือการจัดโครงโปรเจกต์จริง และต่อ database

อ้างอิง