1 · โครงสร้าง: สองท่อกับกลุ่มคนทำงาน
┌─────────────────┐
main ──ส่งงาน──►│ jobs channel │
└────────┬────────┘
│ worker แย่งกันดึงงาน
┌─────────────┼─────────────┐
▼ ▼ ▼
worker 1 worker 2 worker 3 ← มีเท่านี้ ไม่เพิ่ม
│ │ │
└─────────────┼─────────────┘
▼
┌─────────────────┐
│ results channel │──► main รวมผล
└─────────────────┘
งาน 9 ชิ้น · worker 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 มีเพดานว่ารับได้กี่คนพร้อมกัน
ยิงเกินเพดานไม่ได้แปลว่าเร็วขึ้น แต่แปลว่าโดนปฏิเสธ หรือทำระบบเขาล่ม
ก่อนอ่านแถวสุดท้าย — 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 เกินจำเป็น
นี่คือ workshop ท้ายสไลด์ Day 6 — ทำเองก่อน ค่อยเทียบเฉลย
เป้าหมาย output:
✅ Burger ready
✅ Pizza ready
✅ Bubble Tea ready
All orders completed 🎉
ลำดับของสามบรรทัดแรกสลับได้ทุกครั้งที่รัน เพราะ worker 2 ตัว
แย่งงานกัน — เห็น Pizza ขึ้นก่อน Burger ไม่ได้ผิด
เกณฑ์ว่าถูกคือ ครบ 3 บรรทัด ไม่ซ้ำ ไม่ขาด
และ All orders completed 🎉 ต้องอยู่ท้ายสุดเสมอ
- สร้าง
type Order struct { ID int; Menu string }และtype Result struct { Order Order; Ready bool } - Producer: ส่ง 3 ออเดอร์ (Burger, Pizza, Bubble Tea)
เข้า
orderschannel แล้วปิด - Worker: เปิด 2 ตัว ดึงจาก
ordersหน่วงเวลา 300ms แล้วส่งเข้าresults - Main: ปิด
resultsให้ถูกวิธี แล้ววนพิมพ์✅ ... ready· ปิดท้ายด้วยAll orders completed 🎉 - ใช้ directional channel ที่ทุก parameter
- รัน
go vet ./...และgo run -race .— ต้องเงียบทั้งคู่ - ท้าทาย 1: ตั้ง
results := make(chan Result)แบบไม่มี buffer ก่อน แล้วค่อยเอาgo func(){ wg.Wait(); close(results) }()ออก เรียกwg.Wait()ตรง ๆ แทน → โปรแกรมต้องค้าง จดคำในวงเล็บเหลี่ยมมา
จากนั้นเปลี่ยนเป็นmake(chan Result, 3)โดยไม่แก้อย่างอื่น แล้วรันใหม่ — คราวนี้มันไม่ค้าง ทำไม? (ใบ้: กับดักซ้อนในหัวข้อ 4) - ท้าทาย 2 — นับให้ปลอดภัย: เพิ่มตัวนับ
completedที่ทุก worker เพิ่มค่าเอง
ทำสองเวอร์ชัน: แบบไม่ล็อก กับแบบใส่sync.Mutex
รัน-raceทั้งสองแบบ แล้วเทียบผล (ต่อจากบทเรียน 24) - ท้าทาย 3 — ใส่ context: เพิ่ม
ctx context.Contextเป็น parameter ตัวแรกของ worker แล้วให้มันหยุดกลางคันได้ด้วยselect
ตั้งWithTimeout400ms — เสร็จกี่ออเดอร์? (ต่อจากบทเรียน 25) - ท้าทาย 4 — วัดจุดอิ่มตัวเอง: เพิ่มเป็น 60 ออเดอร์
แล้วลอง worker = 1, 2, 5, 10, 20, 60 จับเวลาทุกครั้ง
จากนั้นเปลี่ยน "หน่วงเวลา" เป็น "วนforคำนวณเลข 200 ล้านรอบ" แล้ววัดใหม่ — กราฟต่างกันตรงไหน?
💬 ท้าทาย 4 คือข้อที่ทำให้คุณเลิกเดาตัวเลข worker ไปตลอดชีวิต —
ส่งตัวเลขที่วัดได้มาเทียบกับของผมได้เลย (เครื่องคนละรุ่นจะได้ไม่เท่ากัน
แต่รูปร่างของกราฟควรเหมือนกัน)
จบ concurrency แล้ว — บทสุดท้ายของ Day 6 คือการจัดโครงโปรเจกต์จริง
และต่อ database
อ้างอิง
- Go by Example — Worker Pools — โครงเดียวกับบทนี้ รันได้เลย
- Go Concurrency Patterns: Pipelines and cancellation — Go Blog · อ่านต่อจากบทนี้ได้เลย
- pkg.go.dev — runtime.GOMAXPROCS · NumCPU
- Effective Go — Parallelization
- pkg.go.dev — DB.SetMaxOpenConns — เพดานที่ควรใช้ตั้งจำนวน worker เวลาต่อ database