ใครที่เพิ่งเริ่มเขียน Go น่าจะเคยได้ยินคำว่า “Concurrency” หรือการทำงานแบบพร้อมกันหลายๆ อย่าง ซึ่งเป็นจุดเด่นไม้ตายของภาษานี้เลย แต่พอเริ่มศึกษาจริงๆ หลายคนกลับรู้สึกว่ามันเข้าใจยาก ซับซ้อน และไม่รู้จะเอาไปใช้จริงยังไง
บทความนี้จะพาคุณไปรู้จักกับ Concurrency Patterns พื้นฐาน 6 แบบ ที่โปรแกรมเมอร์ Go ทุกคนควรรู้ โดยเราจะเปลี่ยนเรื่องยากให้กลายเป็นเรื่องง่าย ด้วยการเปรียบเทียบกับสถานการณ์ในชีวิตจริงครับ
ลองจินตนาการว่าคุณเปิดร้านอาหารตามสั่ง:
ใน Go เรามี Goroutine (เปรียบเหมือนเชฟ) ที่สร้างง่ายและกินทรัพยากรน้อยมาก ทำให้เราสามารถสร้างเชฟเป็นหมื่นๆ คนมาช่วยทำงานได้สบายๆ
แต่การมีเชฟเยอะๆ ถ้าบริหารจัดการไม่ดี ก็อาจจะเดินชนกัน วุ่นวายในครัวได้ ดังนั้นเราจึงต้องมี Patterns หรือรูปแบบการทำงานที่เป็นระบบครับ
สถานการณ์: งานเยอะมาก แต่มีคนทำงานจำกัด (เช่น มีจานรอเช็ด 1,000 ใบ แต่มีพนักงานเช็ดแค่ 3 คน)
ถ้าเราปล่อยให้ 3 คนนี้แย่งกันหยิบจานมั่วๆ คงวุ่นวาย Worker Pool คือการจัดระเบียบให้มี “คนงาน” (Workers) จำนวนคงที่ คอยรับงานจาก “สายพาน” (Channel) ไปทำทีละชิ้นจนเสร็จ แล้วค่อยมารับชิ้นต่อไป
Code Example:
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
time.Sleep(time.Second) // สมมติว่าทำงาน 1 วินาที
fmt.Printf("Worker %d finished job %d\n", id, j)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// สร้าง Worker 3 คน (Bounded Concurrency)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// ส่งงาน 5 ชิ้นเข้าสายพาน
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// รอรับผลลัพธ์
for a := 1; a <= 5; a++ {
<-results
}
}
เหมาะสำหรับ: งานที่ต้องประมวลผลเยอะๆ แต่เราไม่อยากให้เครื่องทำงานหนักเกินไป (จำกัดจำนวน Goroutines)
สถานการณ์: มีงานชิ้นใหญ่ที่แตกเป็นงานย่อยๆ ได้ และเราต้องการทำให้เสร็จเร็วที่สุด
Code Example:
func producer(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func merge(cs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
// ฟังก์ชันสำหรับเก็บผลลัพธ์
output := func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}
// รอให้ทุกอย่างเสร็จแล้วปิด channel หลัก
go func() {
wg.Wait()
close(out)
}()
return out
}
// การใช้งาน:
// in := producer(1, 2, 3, 4)
// c1 := square(in)
// c2 := square(in)
// for result := range merge(c1, c2) { ... }
เหมาะสำหรับ: งานที่สามารถทำขนานกันได้ (Parallel processing) เพื่อลดเวลาทำงานรวม
สถานการณ์: สายพานการผลิตในโรงงาน (Assembly Line)
งานชิ้นหนึ่งต้องผ่านหลายขั้นตอนต่อเนื่องกัน เช่น รถยนต์ต้องประกอบโครง -> พ่นสี -> ใส่ล้อ -> ตรวจสอบ เราสามารถให้แต่ละแผนกทำหน้าที่ของตัวเอง แล้วส่งต่อให้แผนกถัดไปทันทีโดยไม่ต้องรอให้เสร็จทั้งหมดก่อน
Concept: Input -> [Stage 1] -> [Stage 2] -> [Stage 3] -> Output
เหมาะสำหรับ: การประมวลผลแบบ Stream หรือข้อมูลที่ไหลมาเรื่อยๆ และต้องผ่านการแปลงหลายขั้นตอน
สถานการณ์: ประตูหมุนทางเข้าสถานีรถไฟฟ้า
ถ้าคนแห่กันรุมเข้ามาพร้อมกัน ประตูอาจพังหรือระบบล่มได้ เราต้องมีตัวกั้นเพื่อให้คนเข้าได้ทีละคน หรือจำกัดจำนวนคนเข้าต่อนาที
Code Example:
func main() {
processRequests := make(chan int, 5)
for i := 1; i <= 5; i++ {
processRequests <- i
}
close(processRequests)
// อนุญาตให้ทำงานได้ทุกๆ 200 มิลลิวินาที (5 requests/second)
limiter := time.Tick(200 * time.Millisecond)
for req := range processRequests {
<-limiter // รอจังหวะสัญญาณ
fmt.Println("Processing request", req, time.Now())
}
}
เหมาะสำหรับ: การป้องกันไม่ให้ API หรือ Database ของเราถูกถล่มด้วย Request ที่มากเกินไป (API Throttling)
สถานการณ์: ที่จอดรถในห้าง มีที่จำกัด 10 คัน
ถ้าที่จอดเต็ม รถคันที่ 11 ต้องรอให้มีรถออกไปก่อนถึงจะเข้าได้ Semaphore คือตัวคุมโควตาว่าอนุญาตให้ทำงานพร้อมกันได้สูงสุดกี่งาน (คล้าย Worker Pool แต่โฟกัสที่การเข้าถึงทรัพยากร)
Code Example:
// สร้าง buffered channel ขนาด 3 (ที่จอด 3 คัน)
sem := make(chan struct{}, 3)
for i := 0; i < 10; i++ {
go func(id int) {
sem <- struct{}{} // เข้าจอด (Acquire)
fmt.Printf("Task %d running...\n", id)
time.Sleep(time.Second)
<-sem // ออกจากที่จอด (Release)
}(i)
}
เหมาะสำหรับ: การจำกัดการเข้าถึง Resource ที่มีจำกัด เช่น Database Connection หรือ File Handle
สถานการณ์: หัวหน้าตะโกนบอก “เลิกงาน! กลับบ้านได้!”
ถ้าเราอยากสั่งให้ Go Routine ที่กำลังทำงานอยู่หยุดทำงานทันที (เช่น ผู้ใช้กดยกเลิก หรือ Timeout) เราจะใช้ Channel พิเศษเรียกว่า done เพื่อส่งสัญญาณบอกให้หยุด
Code Example:
func doWork(done <-chan bool) {
for {
select {
case <-done: // ถ้าได้รับสัญญาณ done
return // จบการทำงาน
default:
// ทำงานต่อไปเรื่อยๆ
}
}
}
เหมาะสำหรับ: Graceful Shutdown หรือการยกเลิก operation ที่กินเวลานาน
Concurrency ใน Go อาจจะดูซับซ้อนในตอนแรก แต่ถ้าเราเข้าใจ Patterns พื้นฐานเหล่านี้ เราจะสามารถเขียนโค้ดที่ทำงานได้รวดเร็วและมีประสิทธิภาพมากขึ้น โดยไม่ต้องปวดหัวกับการจัดการ Thread แบบเดิมๆ
ลองนำ Patterns เหล่านี้ไปปรับใช้กับโปรเจกต์ของคุณดูนะครับ รับรองว่าชีวิตดีขึ้นแน่นอน!
อยากศึกษาเพิ่มเติมเกี่ยวกับ Software Development? ติดตามบทความใหม่ๆ ได้ที่หน้า Blog ของเราครับ