Cloudsoft Co., Ltd
Technology

Go Concurrency Patterns: เรื่องยากที่เข้าใจได้ง่ายๆ ฉบับมือใหม่

Cloudsoft
#Go#Concurrency#Programming#Beginners
Feature image

ใครที่เพิ่งเริ่มเขียน Go น่าจะเคยได้ยินคำว่า “Concurrency” หรือการทำงานแบบพร้อมกันหลายๆ อย่าง ซึ่งเป็นจุดเด่นไม้ตายของภาษานี้เลย แต่พอเริ่มศึกษาจริงๆ หลายคนกลับรู้สึกว่ามันเข้าใจยาก ซับซ้อน และไม่รู้จะเอาไปใช้จริงยังไง

บทความนี้จะพาคุณไปรู้จักกับ Concurrency Patterns พื้นฐาน 6 แบบ ที่โปรแกรมเมอร์ Go ทุกคนควรรู้ โดยเราจะเปลี่ยนเรื่องยากให้กลายเป็นเรื่องง่าย ด้วยการเปรียบเทียบกับสถานการณ์ในชีวิตจริงครับ


Concurrency คืออะไร? (ฉบับห้องครัว)

ลองจินตนาการว่าคุณเปิดร้านอาหารตามสั่ง:

ใน Go เรามี Goroutine (เปรียบเหมือนเชฟ) ที่สร้างง่ายและกินทรัพยากรน้อยมาก ทำให้เราสามารถสร้างเชฟเป็นหมื่นๆ คนมาช่วยทำงานได้สบายๆ

แต่การมีเชฟเยอะๆ ถ้าบริหารจัดการไม่ดี ก็อาจจะเดินชนกัน วุ่นวายในครัวได้ ดังนั้นเราจึงต้องมี Patterns หรือรูปแบบการทำงานที่เป็นระบบครับ


1. Worker Pool Pattern

สถานการณ์: งานเยอะมาก แต่มีคนทำงานจำกัด (เช่น มีจานรอเช็ด 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)


2. Fan-out / Fan-in Pattern

สถานการณ์: มีงานชิ้นใหญ่ที่แตกเป็นงานย่อยๆ ได้ และเราต้องการทำให้เสร็จเร็วที่สุด

  1. Fan-out: จ่ายงานกระจายให้ Worker หลายๆ คนทำพร้อมกัน
  2. Fan-in: รวบรวมผลลัพธ์จากทุกคนกลับมาที่จุดเดียว

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) เพื่อลดเวลาทำงานรวม


3. Pipeline Pattern

สถานการณ์: สายพานการผลิตในโรงงาน (Assembly Line)

งานชิ้นหนึ่งต้องผ่านหลายขั้นตอนต่อเนื่องกัน เช่น รถยนต์ต้องประกอบโครง -> พ่นสี -> ใส่ล้อ -> ตรวจสอบ เราสามารถให้แต่ละแผนกทำหน้าที่ของตัวเอง แล้วส่งต่อให้แผนกถัดไปทันทีโดยไม่ต้องรอให้เสร็จทั้งหมดก่อน

Concept: Input -> [Stage 1] -> [Stage 2] -> [Stage 3] -> Output

เหมาะสำหรับ: การประมวลผลแบบ Stream หรือข้อมูลที่ไหลมาเรื่อยๆ และต้องผ่านการแปลงหลายขั้นตอน


4. Rate Limiter Pattern

สถานการณ์: ประตูหมุนทางเข้าสถานีรถไฟฟ้า

ถ้าคนแห่กันรุมเข้ามาพร้อมกัน ประตูอาจพังหรือระบบล่มได้ เราต้องมีตัวกั้นเพื่อให้คนเข้าได้ทีละคน หรือจำกัดจำนวนคนเข้าต่อนาที

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)


5. Semaphore Pattern

สถานการณ์: ที่จอดรถในห้าง มีที่จำกัด 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


6. Done Channel Pattern

สถานการณ์: หัวหน้าตะโกนบอก “เลิกงาน! กลับบ้านได้!”

ถ้าเราอยากสั่งให้ 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 เหล่านี้ไปปรับใช้กับโปรเจกต์ของคุณดูนะครับ รับรองว่าชีวิตดีขึ้นแน่นอน!

info

อยากศึกษาเพิ่มเติมเกี่ยวกับ Software Development? ติดตามบทความใหม่ๆ ได้ที่หน้า Blog ของเราครับ

← Back to Blog

ให้เราเป็นส่วนหนึ่งในความสำเร็จของคุณ

โทร 0-2153-9499