GoLang: โค้ด Concurrency แบบง่ายๆ ด้วย Goroutine และ Channel
สวัสดีครับทุกคน! วันนี้เรามาคุยเรื่อง GoLang กันบ้างดีกว่า คือหลายคนอาจจะยังไม่เคยลอง หรือเคยลองแล้วแต่ยังงงๆ ว่าไอ้ Goroutine กับ Channel ที่เค้าพูดถึงกันนักหนาเนี่ย มันดียังไง?
ยอมรับเลยว่าตอนแรกผมก็ไม่ได้อินอะไรกับ Go มากนะ แต่พอได้มาลองเขียนโค้ดที่มันต้องทำงานพร้อมๆ กัน (Concurrency) เยอะๆ อ่ะ โห... Go นี่ตอบโจทย์มากจริงๆ เพราะมันออกแบบมาให้ทำเรื่องพวกนี้ง่ายตั้งแต่แรกเลย ไม่ต้องมานั่งจัดการ Thread วุ่นวายเหมือนภาษาอื่น
Goroutine มันคืออะไร?
คิดง่ายๆ ว่า Goroutine มันก็คือ function ที่รันพร้อมๆ กันไปกับ main function ของเรานั่นแหละครับ แต่มันเบากว่า Thread ทั่วไปเยอะมากๆ เบาจนแบบ เราสร้างเป็นแสนๆ ตัวมันก็ยังไหวอะ! ไม่ต้องกลัวเครื่องจะค้าง
วิธีสร้าง Goroutine ก็โคตรง่าย แค่เติม go เข้าไปหน้า function call แค่นั้นเอง
package main
import (
"fmt"
"time"
)
func sayHello() {
time.Sleep(2 * time.Second) // แกล้งทำเป็นว่าทำงานนานๆ หน่อย
fmt.Println("สวัสดีครับจาก Goroutine!")
}
func main() {
go sayHello() // เรียก function sayHello ให้รันเป็น Goroutine
fmt.Println("โปรแกรมหลักรันต่อเลยนะ ไม่ได้รอ")
time.Sleep(3 * time.Second) // รอให้ Goroutine ทำงานเสร็จก่อน ไม่งั้นโปรแกรมหลักจบไปก่อน
fmt.Println("โปรแกรมหลักจบแล้ว.")
}
ถ้าลองรันโค้ดข้างบน จะเห็นว่า โปรแกรมหลักรันต่อเลยนะ ไม่ได้รอ จะโผล่มาก่อน สวัสดีครับจาก Goroutine! นั่นแหละครับคือการทำงานแบบ Concurrency!
ปัญหานิดหน่อย: สังเกตว่าผมต้องใส่ time.Sleep ไว้ใน main ด้วย ไม่งั้นโปรแกรมมันจะจบไปก่อนที่ sayHello จะทำงานเสร็จอะ เพราะ Goroutine มันไม่ได้บล็อค main ไง ทีนี้จะสื่อสารกันยังไงล่ะ? ก็ต้องใช้ Channel นี่แหละ
Channel คืออะไร?
Channel ก็เหมือนท่อ หรือช่องทางสื่อสารระหว่าง Goroutine นั่นแหละครับ มันช่วยให้ Goroutine ส่งข้อมูลถึงกันได้แบบปลอดภัย และยังเป็นวิธีที่ดีในการซิงค์การทำงานด้วย
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d กำลังประมวลผลงาน %d\n", id, j)
time.Sleep(time.Second) // แกล้งทำว่าใช้เวลาประมวลผล
results <- j * 2 // ส่งผลลัพธ์กลับไป
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs) // สร้าง channel สำหรับงานที่จะส่ง
results := make(chan int, numJobs) // สร้าง channel สำหรับผลลัพธ์
// สร้าง Goroutine worker 3 ตัว
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// ส่งงานไปให้ worker
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // บอกว่าไม่มีงานเพิ่มแล้วนะ ปิด channel งาน
// รอรับผลลัพธ์จาก worker
for a := 1; a <= numJobs; a++ {
<-results
}
// ไม่จำเป็นต้อง close(results) เพราะเดี๋ยวโปรแกรมก็จบละ
fmt.Println("ทุกงานเสร็จสมบูรณ์!")
}
ในตัวอย่างนี้: * เราสร้าง jobs channel เพื่อส่งงาน (เลขจำนวนเต็ม) และ results channel เพื่อรับผลลัพธ์ * มี worker Goroutine 3 ตัว คอยรับงานจาก jobs และส่งผลลัพธ์ไปที่ results * <-chan int คือ channel ที่รับได้แค่ (receive-only) * chan<- int คือ channel ที่ส่งได้อย่างเดียว (send-only) * close(jobs) สำคัญมาก! ถ้าไม่ปิด jobs channel ตัว for j := range jobs ใน worker มันจะค้างรอไปเรื่อยๆ ครับ เพราะมันไม่รู้ว่างานหมดแล้วรึยัง
ข้อควรระวัง (ที่ผมเคยพลาดบ่อยๆ): * Deadlock: ถ้า channel ไม่มีใครส่ง ไม่มีใครรับ หรือรับไม่ครบ จบเลยครับ! โปรแกรมจะค้าง หรือ panic ว่า fatal error: all goroutines are asleep - deadlock! บ่อยมากที่ลืม close channel หรือลืมรับค่าจนครบอะ * Buffer Size: make(chan int, 5) คือการสร้าง channel ที่มี buffer 5 ช่อง ถ้าไม่ใส่ buffer หรือใส่ make(chan int) เฉยๆ มันจะเป็น unbuffered channel ที่ต้องมีคนรอรับทันทีที่ส่ง ไม่งั้นก็บล็อคเหมือนกัน อันนี้แล้วแต่ use case นะ แต่ถ้าใช้แล้วงงๆ ลองคิดถึง buffer ดูครับ
ส่วนตัวผมมองว่า Go นี่แหละคืออนาคตของงานที่ต้องการ Concurrency สูงๆ เพราะมันใช้ง่าย เรียนรู้ไม่ยาก แถม performance ก็ดีมากๆ ด้วย ถ้ามีโปรเจคที่ต้องทำอะไรพร้อมๆ กันเยอะๆ หรือสร้าง API ที่ต้องการความเร็ว Go เป็นตัวเลือกที่ไม่ควรมองข้ามเลยจริงๆ ครับ ลองเอาไปเล่นดูนะ!