ทำความเข้าใจเรื่อง Concurrency ในแบบที่เห็นภาพชัด
เวลาเราเขียนโปรแกรมแบบปกติ คอมพิวเตอร์จะทำงานทีละอย่างตามลำดับจากบนลงล่าง ถ้าต้องรออะไรนานๆ เช่น รอโหลดข้อมูลจากเว็บไซต์อื่น โปรแกรมของเราก็จะค้างหรือหยุดนิ่งไปเลยจนกว่างานนั้นจะเสร็จ นี่คือสิ่งที่เรียกว่า Sequential Programming (การเขียนโปรแกรมแบบเป็นลำดับขั้น) ซึ่งอาจไม่ตอบโจทย์ในยุคที่ทุกอย่างต้องรวดเร็ว
Concurrency (การทำงานหลายอย่างสลับกันไปมาในเวลาเดียวกัน) คือทางออกที่ช่วยให้โปรแกรมเราฉลาดขึ้น ลองนึกภาพว่าคุณกำลังทำอาหาร ถ้าต้องรอให้น้ำเดือดแล้วค่อยเริ่มหั่นผัก คุณจะเสียเวลาเปล่า แต่ถ้าคุณหั่นผักไปพร้อมกับรอน้ำเดือด นั่นแหละคือแนวคิดของมัน โปรแกรมเมอร์จะใช้เทคนิคนี้เพื่อจัดการงานหลายอย่างโดยไม่ทำให้ผู้ใช้งานต้องรอคอยอย่างเสียเปล่า
ในภาษา Go เรามีเครื่องมือที่ทรงพลังมากชื่อว่า Goroutines (หน่วยประมวลผลขนาดเล็กที่ภาษา Go จัดการให้เอง) มันช่วยให้เราสั่งงานให้ทำงานแบบคู่ขนานได้ง่ายมาก โดยไม่ต้องกังวลเรื่องการจัดการทรัพยากรเครื่องที่ซับซ้อนเหมือนภาษาอื่น นี่คือหัวใจสำคัญที่ทำให้ Go เป็นภาษาโปรแกรมยอดนิยมในการสร้างระบบที่รองรับผู้ใช้งานจำนวนมาก
Goroutines: ก้าวแรกสู่การทำงานแบบคู่ขนาน
Goroutine คือวิธีรันฟังก์ชันให้ทำงานแยกออกมาจากโปรแกรมหลัก โดยเราแค่ใส่คำสั่ง go ไว้หน้าฟังก์ชันเท่านั้น โปรแกรมก็จะรันฟังก์ชันนั้นในพื้นหลังทันทีโดยไม่รอให้งานชิ้นนั้นเสร็จก่อนแล้วค่อยไปทำบรรทัดถัดไป นี่คือความง่ายที่ทำให้มือใหม่หลายคนประทับใจตั้งแต่ครั้งแรกที่ลองเขียน
ลองดูตัวอย่างพื้นฐานนี้เพื่อให้เห็นภาพชัดเจนขึ้น เราจะสร้างฟังก์ชันที่พิมพ์ข้อความออกมา แล้วเรียกมันแบบปกติเทียบกับการเรียกแบบ Goroutine สังเกตว่าถ้าเราไม่สั่งให้โปรแกรมรอ มันอาจจะจบการทำงานก่อนที่ Goroutine จะทำงานเสร็จเสียอีก เพราะโปรแกรมหลักไม่ได้สนใจว่า Goroutine จะทำอะไรต่อ
package main
import (
"fmt"
"time"
)
func sayHi() {
fmt.Println("สวัสดีจาก Goroutine!")
}
func main() {
// สั่งให้ sayHi ทำงานแยกออกมา
go sayHi()
// รอเวลาเล็กน้อยเพื่อให้ Goroutine ทำงานเสร็จ
time.Sleep(1 * time.Second)
}
ในโค้ดนี้ go sayHi() คือการสั่งให้ฟังก์ชันทำงานในฐานะ Goroutine ส่วน time.Sleep คือการสั่งให้โปรแกรมหลักหยุดรอ 1 วินาที เพื่อให้ Goroutine มีเวลาพิมพ์ข้อความออกมา หากเราลบบรรทัด time.Sleep ออก โปรแกรมอาจปิดตัวลงทันทีโดยไม่แสดงผลอะไรเลย ผลลัพธ์ที่ได้คือข้อความ "สวัสดีจาก Goroutine!" จะถูกพิมพ์ออกมาทางหน้าจอ
มือใหม่มักพลาดเรื่องการจบโปรแกรมก่อนที่ Goroutine จะทำงานเสร็จ จำไว้เสมอว่า Goroutine ไม่ได้การันตีว่าจะทำงานจบเมื่อไหร่ เราต้องมีวิธีจัดการหรือรอคอยมันอย่างถูกต้อง ในโปรเจกต์จริงเราจะไม่ใช้ time.Sleep แบบนี้ แต่จะใช้เครื่องมืออื่นที่เหมาะสมกว่าซึ่งเราจะเรียนรู้กันในหัวข้อถัดไป
Channels: ทางเชื่อมส่งต่อข้อมูลระหว่างงาน
เมื่อ Goroutine ทำงานแยกกันแล้ว คำถามต่อมาคือพวกมันจะคุยกันได้อย่างไร เพราะถ้าแยกกันอยู่ต่างคนต่างทำ ข้อมูลก็คงไม่ถึงกัน Channels (ช่องทางสำหรับส่งข้อมูลระหว่าง Goroutine) จึงถูกสร้างมาเพื่อแก้ปัญหานี้ เปรียบเสมือนท่อส่งข้อมูลที่ช่วยให้ Goroutine ส่งค่าไปหากันได้อย่างปลอดภัย
การสร้าง Channel ทำได้ง่ายๆ โดยใช้คำสั่ง make(chan ประเภทข้อมูล) เมื่อมีท่อแล้ว เราสามารถส่งข้อมูลเข้าท่อด้วยเครื่องหมาย <- และรับข้อมูลออกมาด้วยเครื่องหมายเดียวกัน การทำแบบนี้ช่วยให้เราไม่ต้องกังวลเรื่องการแย่งกันเข้าถึงข้อมูล ซึ่งเป็นปัญหาใหญ่ในการเขียนโปรแกรมแบบคู่ขนานที่เรียกว่า Race Condition (สภาวะที่การทำงานผิดพลาดเพราะแย่งกันแก้ไขข้อมูล)
ลองดูตัวอย่างการส่งข้อความผ่าน Channel กันครับ เราจะสร้าง Goroutine เพื่อส่งข้อความเข้าไปในท่อ แล้วให้โปรแกรมหลักรับข้อความนั้นออกมาแสดงผล วิธีนี้เป็นวิธีมาตรฐานที่โปรแกรมเมอร์มืออาชีพใช้สื่อสารระหว่างงานที่ทำพร้อมกัน เพื่อให้ข้อมูลถูกส่งผ่านอย่างถูกต้องและเป็นระเบียบ
package main
import "fmt"
func main() {
// สร้างท่อสำหรับส่งข้อความชนิดสตริง
messages := make(chan string)
// สั่งให้ฟังก์ชันทำงานเพื่อส่งข้อมูลเข้าท่อ
go func() {
messages <- "ส่งข้อมูลสำเร็จแล้ว!"
}()
// รับข้อมูลจากท่อแล้วเก็บไว้ในตัวแปร
msg := <-messages
fmt.Println(msg)
}
ในโค้ดนี้ make(chan string) คือการสร้างช่องทางสื่อสาร messages <- คือการยัดข้อมูลลงในท่อ และ <-messages คือการดึงข้อมูลออกมา โปรแกรมจะรอจนกว่าจะมีข้อมูลส่งมาในท่อโดยอัตโนมัติ ผลลัพธ์ที่ได้คือข้อความ "ส่งข้อมูลสำเร็จแล้ว!" จะถูกพิมพ์ออกมาทางหน้าจออย่างแม่นยำ
ข้อควรระวังคือถ้าไม่มีใครมารับข้อมูลจาก Channel หรือไม่มีใครส่งข้อมูลเข้ามา โปรแกรมจะค้างหรือเกิดข้อผิดพลาดที่เรียกว่า Deadlock (สถานการณ์ที่โปรแกรมหยุดชะงักเพราะรอคอยกันไปมาอย่างไม่มีที่สิ้นสุด) ดังนั้นการวางแผนว่าจะส่งและรับข้อมูลตอนไหนจึงเป็นเรื่องสำคัญมากสำหรับโปรแกรมเมอร์ทุกคน
Worker Pools: การจัดการงานจำนวนมากให้เป็นระบบ
ถ้าคุณต้องทำงาน 1,000 อย่างพร้อมกัน การเปิด Goroutine 1,000 อันในคราวเดียวอาจทำให้เครื่องทำงานหนักเกินไป เราจึงนิยมใช้ Worker Pool (กลุ่มของคนงานที่คอยหยิบงานในคิวมาทำ) เพื่อจำกัดจำนวนงานที่ทำพร้อมกันให้พอดีกับทรัพยากรที่มีอยู่
แนวคิดนี้เหมือนกับเคาน์เตอร์ธนาคารที่มีพนักงาน 3 คน แต่มีลูกค้า 10 คนรอคิวอยู่ แทนที่ลูกค้าทุกคนจะพุ่งเข้าหาพนักงานพร้อมกัน ลูกค้าจะเข้าคิวและเดินไปหาพนักงานเมื่อพนักงานคนไหนว่าง การทำแบบนี้ช่วยให้ระบบของเรามีความเสถียรและไม่ล่มเมื่อมีงานเข้ามาเยอะเกินไป
ในการเขียน Go เราจะใช้ Channel เป็นคิวงาน และใช้ Goroutine เป็นพนักงานที่คอยดึงงานจากคิวมาทำ นี่เป็นเทคนิคที่สำคัญมากสำหรับงานจำพวกการประมวลผลไฟล์จำนวนมาก การส่งอีเมล หรือการดึงข้อมูลจากหลายๆ เว็บไซต์พร้อมกัน ซึ่งเป็นงานที่โปรแกรมเมอร์สาย Backend (ระบบหลังบ้าน) ต้องเจอเป็นปกติ
Synchronization: หัวใจของการควบคุมความถูกต้อง
แม้ว่า Goroutine จะทำงานแยกกันได้ แต่บางครั้งเราก็ต้องรอให้ทุกอย่างเสร็จก่อนถึงจะทำงานขั้นตอนถัดไปได้ เราเรียกสิ่งนี้ว่า Synchronization (การทำให้งานหลายอย่างประสานเวลาให้ตรงกัน) เครื่องมือที่นิยมที่สุดคือ WaitGroup (ตัวนับจำนวนงานที่รอให้เสร็จ)
ถ้าคุณไม่มี WaitGroup คุณจะไม่มีทางรู้เลยว่า Goroutine ทั้ง 10 ตัวที่คุณสั่งไปนั้นทำงานเสร็จครบหรือยัง WaitGroup จะทำหน้าที่นับจำนวนงานที่ค้างอยู่ เมื่อ Goroutine ทำงานเสร็จก็จะแจ้งว่า "ฉันเสร็จแล้วนะ" เมื่อตัวนับลดลงจนถึงศูนย์ โปรแกรมหลักถึงจะดำเนินการต่อได้
การใช้ WaitGroup ช่วยให้โปรแกรมของเรามีความน่าเชื่อถือ คุณจะไม่ต้องเดาเวลาด้วย time.Sleep อีกต่อไป ซึ่งเป็นวิธีที่อันตรายและไม่เป็นมืออาชีพ การฝึกใช้เครื่องมือเหล่านี้ให้คล่องจะช่วยให้คุณก้าวข้ามจากมือใหม่ไปสู่การเป็นโปรแกรมเมอร์ที่เขียนโค้ดได้อย่างมีประสิทธิภาพและปลอดภัย
สรุป: เปลี่ยนวิธีคิดเพื่อสร้างแอปพลิเคชันที่ทรงพลัง
การเรียนรู้เรื่อง Concurrency ใน Go ไม่ใช่แค่การจำคำสั่ง go หรือ chan แต่คือการเปลี่ยนวิธีคิดจากการมองงานเป็นเส้นตรง ไปเป็นการมองงานเป็นชุดของหน่วยย่อยที่ทำงานร่วมกันได้ เมื่อคุณเข้าใจหลักการนี้ คุณจะสามารถสร้างระบบที่รองรับการใช้งานหนักๆ ได้เหมือนกับบริษัทระดับโลกที่ใช้ Go ในการจัดการข้อมูลมหาศาล
ลองนึกถึงโปรเจกต์ที่คุณกำลังทำอยู่ตอนนี้ ถ้าคุณมีงานที่ต้องทำซ้ำๆ เช่น การดึงข้อมูลจาก API หลายตัว ลองนำ Goroutine ไปประยุกต์ใช้ดูครับ เริ่มจากการทำสิ่งเล็กๆ เช่น เขียนโปรแกรมดึงข้อมูลจาก 3 เว็บไซต์พร้อมกันแทนที่จะดึงทีละอัน คุณจะเห็นความแตกต่างของความเร็วอย่างชัดเจนทันที
การเป็นโปรแกรมเมอร์ที่เก่งไม่ได้วัดกันที่จำนวนโค้ดที่เขียน แต่คือการเลือกใช้เครื่องมือที่เหมาะสมกับงานมากที่สุด Concurrency คือพลังพิเศษที่คุณมีในภาษา Go จงฝึกฝนและทำความเข้าใจมันให้ลึกซึ้ง เพราะทักษะนี้จะเป็นอาวุธสำคัญที่จะทำให้คุณโดดเด่นในตลาดงานสายพัฒนาซอฟต์แวร์อย่างแน่นอนครับ
ที่มา: Go Concurrency Explained: How Goroutines Can Change the Way You Build Applications — DEV Community