เข้าใจเรื่อง Concurrency และทำไมโปรแกรมเมอร์ต้องรู้จัก
ถ้าเราพูดถึงการเขียนโปรแกรมให้ทำงานหลายอย่างพร้อมกัน หลายคนอาจนึกถึงคำว่า Concurrency (การจัดการงานหลายอย่างในช่วงเวลาเดียวกัน) ซึ่งเป็นทักษะสำคัญที่มือใหม่ควรรู้ ภาษาอย่าง Go (ภาษาโปรแกรมที่เน้นความเร็วและการจัดการงานพร้อมกัน) ถูกออกแบบมาเพื่อแก้ปัญหาเรื่องการรอคอยงานที่ช้า เช่น การดึงข้อมูลจากเว็บไซต์อื่น หรือการอ่านไฟล์ขนาดใหญ่ แทนที่จะให้โปรแกรมหยุดรอทีละงาน เราสามารถสั่งให้มันทำงานหลายอย่างไปพร้อมกันได้เลย
ลองจินตนาการว่าคุณเป็นพ่อครัวในร้านอาหาร ถ้าคุณต้องรอให้ขนมปังปิ้งเสร็จก่อนถึงจะเริ่มทอดไข่ คุณคงทำอาหารไม่ทันกิน แต่ถ้าคุณใส่ขนมปังลงเครื่องปิ้งแล้วหันมาทอดไข่ทันที นั่นแหละคือแนวคิดของ Concurrency ในโลกการเขียนโปรแกรม เราไม่ได้ทำทุกอย่างพร้อมกันในเสี้ยววินาทีเดียวแบบเป๊ะๆ แต่เราฉลาดพอที่จะไม่ปล่อยให้ CPU (หน่วยประมวลผลกลาง) นั่งว่างระหว่างรอผลจากงานอื่น
การเข้าใจเรื่องนี้จะช่วยให้แอปพลิเคชันที่คุณสร้างขึ้นมีประสิทธิภาพมากขึ้น ไม่ว่าจะเป็นงานด้าน Backend (ระบบหลังบ้านที่ทำงานบนเซิร์ฟเวอร์) หรือการจัดการข้อมูลจำนวนมาก เมื่อคุณก้าวเข้าสู่สายงานโปรแกรมเมอร์ การเขียนโค้ดที่รันแบบลำดับขั้นเดียว (Sequential) อาจไม่เพียงพอต่อความต้องการของระบบสมัยใหม่ การเรียนรู้เครื่องมือใน Go จึงเป็นก้าวสำคัญที่จะเปลี่ยนให้คุณเป็นนักพัฒนาที่เก่งขึ้นอย่างเห็นได้ชัด
รู้จักกับ Goroutine หัวใจสำคัญของการทำงานแบบคู่ขนาน
Goroutine (ฟังก์ชันที่ทำงานแยกออกมาเป็นอิสระ) คือสิ่งแรกที่คุณต้องรู้จัก มันเปรียบเสมือนหน่วยงานย่อยที่ Go สร้างขึ้นมาเพื่อรับผิดชอบงานหนึ่งอย่างโดยเฉพาะ โดยใช้ทรัพยากรน้อยมากเมื่อเทียบกับการสร้างกระบวนการทำงานใหม่ในภาษาอื่น ทำให้เราสามารถเปิด Goroutine ได้เป็นหลักหมื่นหลักแสนโดยที่คอมพิวเตอร์ไม่ค้าง
วิธีการใช้งานนั้นง่ายมาก เพียงแค่เติมคำสั่ง go ไว้หน้าฟังก์ชันที่ต้องการให้ทำงานแยกออกไป โปรแกรมหลักจะรันบรรทัดถัดไปทันทีโดยไม่รอให้ฟังก์ชันนั้นทำงานเสร็จ นี่คือจุดที่มือใหม่มักสับสน เพราะถ้าคุณสั่งให้โปรแกรมจบการทำงานก่อนที่ Goroutine จะทำงานเสร็จ งานนั้นก็จะไม่แสดงผลออกมาให้เห็น
ลองดูตัวอย่างการใช้งานเบื้องต้นกันครับ เราจะสั่งให้ฟังก์ชันทำงานในพื้นหลังขณะที่โปรแกรมหลักยังทำงานต่อเนื่องไปเรื่อยๆ
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("สวัสดีจาก Goroutine!")
}
func main() {
go sayHello() // สั่งให้ฟังก์ชันทำงานแยกออกมา
fmt.Println("โปรแกรมหลักทำงานต่อ")
time.Sleep(1 * time.Second) // รอให้ Goroutine ทำงานเสร็จก่อนจบ
}
ในโค้ดนี้ บรรทัด go sayHello() คือการสั่งให้ Go สร้าง Goroutine ขึ้นมาทำงานแยกต่างหาก ส่วน time.Sleep ถูกใส่เข้ามาเพื่อให้โปรแกรมหลักรอสักครู่ก่อนจะปิดตัวลง เพื่อให้เรามั่นใจว่าข้อความจาก Goroutine จะถูกพิมพ์ออกมาทันเวลา
ผลลัพธ์ที่ควรเห็นคือ: "โปรแกรมหลักทำงานต่อ" แล้วตามด้วย "สวัสดีจาก Goroutine!" (ลำดับอาจสลับกันได้ขึ้นอยู่กับการจัดการของระบบ)
เชื่อมต่อและสื่อสารด้วย Channels
เมื่อเรามีหลายงานทำงานพร้อมกัน ปัญหาที่ตามมาคือเราจะรู้ได้อย่างไรว่างานนั้นเสร็จหรือยัง หรือจะส่งข้อมูลหากันได้อย่างไร Channels (ท่อสำหรับส่งข้อมูลระหว่าง Goroutine) คือคำตอบครับ มันทำหน้าที่เป็นเหมือนทางเดินข้อมูลที่ปลอดภัย ทำให้ Goroutine สองตัวคุยกันได้โดยไม่ต้องกังวลเรื่องข้อมูลชนกัน
การสร้าง Channel ทำได้โดยใช้คำสั่ง make(chan type) โดยเราต้องระบุชนิดข้อมูลที่จะส่งผ่านท่อนี้ เช่น chan int สำหรับส่งตัวเลข หรือ chan string สำหรับข้อความ เมื่อมีการส่งข้อมูลลงไปใน Channel ผู้ส่งจะหยุดรอจนกว่าจะมีคนมารับข้อมูลนั้นไป นี่คือกลไกพื้นฐานที่ป้องกันไม่ให้โปรแกรมพังจากการเข้าถึงข้อมูลพร้อมกัน
มาดูตัวอย่างการส่งข้อมูลกลับจาก Goroutine มายังโปรแกรมหลักกันครับ
package main
import "fmt"
func worker(c chan string) {
c <- "ทำงานเสร็จแล้ว!" // ส่งข้อมูลเข้า Channel
}
func main() {
messages := make(chan string) // สร้าง Channel
go worker(messages)
msg := <-messages // รับข้อมูลจาก Channel
fmt.Println(msg)
}
ในโค้ดนี้ chan string คือท่อส่งข้อมูลประเภทข้อความ บรรทัด c <- "..." คือการใส่ข้อมูลลงไป ส่วน <-messages คือการดึงข้อมูลออกมา ถ้ายังไม่มีใครส่งข้อมูลมา บรรทัดนี้จะหยุดรอ (Block) ไว้ก่อนจนกว่าจะมีข้อมูลเข้ามาจริงๆ
ผลลัพธ์ที่ได้คือ: "ทำงานเสร็จแล้ว!" ซึ่งเป็นการยืนยันว่าโปรแกรมหลักได้รับข้อมูลจาก Goroutine เรียบร้อยแล้ว
จัดการเวลาและยกเลิกงานด้วย Context
ในสถานการณ์จริง งานที่เราสั่งให้ทำอาจจะค้างหรือใช้เวลานานเกินไป เราจึงต้องมีตัวควบคุมที่ชื่อว่า Context (ตัวช่วยส่งสัญญาณยกเลิกหรือกำหนดเวลาให้งาน) มันช่วยให้เราบอก Goroutine ได้ว่า "ถ้าผ่านไป 2 วินาทีแล้วงานไม่เสร็จ ให้หยุดทำทันทีนะ" เพื่อไม่ให้เซิร์ฟเวอร์ของเราต้องรอคอยอย่างไร้จุดหมาย
การใช้ Context เป็นแนวทางปฏิบัติที่ดีมากสำหรับมือใหม่ที่ต้องการเขียนโปรแกรมแบบมืออาชีพ เพราะมันป้องกันปัญหาเรื่องการค้างของระบบ (Deadlock หรือ Resource leak) โดยเฉพาะเวลาที่เราต้องต่อกับ API (ช่องทางเชื่อมต่อข้อมูลระหว่างโปรแกรม) ภายนอกที่อาจจะล่มหรือตอบกลับช้า
ตัวอย่างการใช้ context.WithTimeout เพื่อกำหนดเวลาทำงาน:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() // ปิดการใช้งานเมื่อเสร็จสิ้น
select {
case <-time.After(2 * time.Second):
fmt.Println("งานใช้เวลานานเกินไป")
case <-ctx.Done():
fmt.Println("ยกเลิกงานเพราะเกินเวลา:", ctx.Err())
}
}
ในตัวอย่างนี้ เราตั้งเวลาไว้ 1 วินาที แต่ตัวงานจำลอง (time.After) ใช้เวลา 2 วินาที ผลที่ได้คือโปรแกรมจะเลือกเคส ctx.Done() ก่อนเพราะเวลาหมดลงตามกำหนดการของ Context
ผลลัพธ์คือ: "ยกเลิกงานเพราะเกินเวลา: context deadline exceeded" ซึ่งเป็นการเตือนว่างานไม่สามารถทำเสร็จในเวลาที่กำหนด
สร้าง Worker Pool เพื่อคุมจำนวนงาน
มือใหม่หลายคนมักพลาดด้วยการสร้าง Goroutine จำนวนมหาศาลพร้อมกันโดยไม่จำกัด ซึ่งอาจทำให้หน่วยความจำเต็มได้ วิธีที่ดีกว่าคือการใช้ Worker Pool (กลุ่มของตัวทำงานที่มีจำนวนจำกัด) คล้ายกับการจ้างพนักงานจำนวนคงที่มาคอยหยิบงานจากกองเอกสารไปทำทีละชิ้น
หลักการคือเราจะสร้าง Goroutine ขึ้นมาจำนวนหนึ่งตามที่เรากำหนด (เช่น 5 ตัว) แล้วส่งงานผ่าน Channel เข้าไปให้พนักงานเหล่านี้แย่งกันทำ วิธีนี้ช่วยให้เราควบคุมการใช้ทรัพยากรของเครื่องได้แม่นยำมาก และยังเป็นมาตรฐานที่ใช้กันในงาน Backend ระดับองค์กร
ขั้นตอนการทำ Worker Pool:
- สร้าง Channel สำหรับรับงาน (Jobs) และส่งผลลัพธ์ (Results)
- สร้างฟังก์ชัน Worker ที่คอยวนลูปรับงานจาก Channel
- สั่งให้ Goroutine ทำงานตามจำนวนที่ต้องการ
- ส่งข้อมูลงานเข้าสู่ Channel และรอรับผลลัพธ์
การทำแบบนี้ช่วยให้ระบบมีเสถียรภาพสูง เพราะไม่ว่าจะมีงานเข้ามามากแค่ไหน พนักงานของเราก็จะมีจำนวนเท่าเดิม ไม่ทำให้เซิร์ฟเวอร์ล่มจากการเปิด Goroutine มากเกินไป
สรุป
การเขียนโปรแกรมด้วย Go Concurrency ไม่ใช่เรื่องยากอย่างที่คิด หากคุณค่อยๆ ฝึกจาก Goroutine เพื่อรันงานแยกส่วน แล้วใช้ Channels เป็นท่อสื่อสารที่ปลอดภัย สุดท้ายเมื่อต้องควบคุมเวลาหรือยกเลิกงาน ก็มี Context เป็นตัวช่วยจัดการที่ทรงพลัง สิ่งเหล่านี้คือหัวใจสำคัญของภาษา Go ที่ทำให้มันกลายเป็นภาษายอดนิยมในงานระบบหลังบ้าน
ลองนำแนวคิดนี้ไปปรับใช้กับโปรเจกต์ของคุณดูครับ เช่น ถ้าคุณกำลังทำแอปฯ ดึงข้อมูลราคาหุ้นจาก 3 เว็บไซต์พร้อมกัน แทนที่จะดึงทีละเว็บ ลองใช้ Goroutine ดึงข้อมูลทั้ง 3 เว็บพร้อมกันแล้วส่งผลลัพธ์กลับมาทาง Channel คุณจะพบว่าโปรแกรมของคุณทำงานได้เร็วขึ้นอย่างน่าทึ่ง
การหมั่นฝึกฝนและทำความเข้าใจเรื่องนี้ให้ถ่องแท้ จะทำให้คุณก้าวผ่านระดับมือใหม่ไปสู่โปรแกรมเมอร์ที่เขียนโค้ดได้มีประสิทธิภาพและรองรับการใช้งานหนักๆ ได้อย่างมั่นใจ ขอให้สนุกกับการเขียนโค้ดและพัฒนาตัวเองต่อไปครับ!
ที่มา: Go Concurrency: How to Run Parallel Tasks with Goroutines, Channels, and Context — DEV Community