Concurrency ใน Go ตอนที่ 1: Basic & Synchronization
Source: Dev.to
พื้นฐาน Concurrency และ Synchronization
ก่อนอื่นมาทำความเข้าใจสองคำนี้กันก่อน
Concurrency
นิยามคือ ความสามารถในการจัดการงานหลายอย่างในช่วงเวลาเดียวกัน
การทำงานแต่ละงานไม่จำเป็นต้องเกิดขึ้นพร้อมกัน เพียงสลับงานไปมาก็ถือว่าเป็น concurrency
ลักษณะสำคัญคือ ช่วงเวลาเริ่มต้นของแต่ละงานมีการ overlap
Parallelism
นิยามคือ ความสามารถในการประมวลผลงานหลายอย่างพร้อมกัน ต้องอาศัย hardware
หมายเหตุ: concurrency สามารถเป็น parallelism หรือไม่ก็ได้ ขึ้นกับการแมปกับ hardware
แม้ว่าไม่ได้รันแบบ parallel แต่การรัน concurrent ก็สามารถทำให้ทำงานได้เร็วขึ้น เพราะในงานทั่วไปมักต้องรออะไรบางอย่าง เช่น การดึงหรือเก็บค่าจาก memory การสลับไปทำงานอื่นในช่วงรอทำให้ประสิทธิภาพโดยรวมดีขึ้น
Concurrency ใน Go
Go มี concurrency แบบ built‑in ไม่ต้องพึ่ง library ภายนอก
GOMAXPROCS
กำหนดจำนวน OS threads ที่สามารถประมวลผล Go code ได้พร้อมกัน
runtime.GOMAXPROCS(4) // ตั้งค่าจากโค้ด
หรือผ่าน environment variable
export GOMAXPROCS=4
ตั้งแต่ Go 1.5 เป็นต้นไป ค่าเริ่มต้นของ GOMAXPROCS เท่ากับจำนวน Logical CPUs ของเครื่อง จึงไม่จำเป็นต้องแก้ไข หากตั้งค่าสูงเกินไปอาจทำให้ context switching เพิ่มขึ้นและทำให้ทำงานช้าลง
Goroutine
Go ใช้ goroutine ซึ่งเป็น lightweight thread (อาจมีหลายพันหรือหลายหมื่นภายใน thread เดียว) และใช้ Go Runtime Scheduler จัดตารางงาน
เมื่อโปรแกรมเริ่ม main() Go จะสร้าง goroutine ตัวแรกที่เรียกว่า main routine หากต้องการทำงานแบบ concurrent เพียงใส่คีย์เวิร์ด go ข้างหน้าการเรียกฟังก์ชัน
ตัวอย่างพื้นฐาน
package main
import (
"fmt"
"time"
)
func main() {
go fmt.Println("New Routine")
fmt.Println("Main Routine")
}
ผลลัพธ์จะเห็นเพียง
Main Routine
เพราะ main() จบก่อนที่ goroutine ใหม่จะทำงาน
วิธีรอ goroutine อย่างถูกต้อง
การใช้ time.Sleep ไม่แนะนำ เพราะไม่รู้ว่าจะต้องรอกี่เวลา วิธีที่เหมาะสมคือใช้กลไก synchronization เช่น sync.WaitGroup
Synchronization ด้วยแพ็กเกจ sync
sync.WaitGroup
WaitGroup เก็บตัวนับจำนวนงานที่ต้องรอ
Add(n)– เพิ่มตัวนับDone()– ลดตัวนับเมื่องานเสร็จWait()– รอจนตัวนับเป็น 0
ตัวอย่าง
package main
import (
"fmt"
"sync"
)
func foo(wg *sync.WaitGroup) {
fmt.Println("New Routine")
wg.Done() // บอกว่ารันจบแล้ว
}
func main() {
var wg sync.WaitGroup
wg.Add(2) // มี 2 งานที่ต้องรอ
go foo(&wg)
go foo(&wg)
wg.Wait() // รอจนกว่างานทั้งหมดเสร็จ
fmt.Println("Main Routine")
}
ผลลัพธ์
New Routine
New Routine
Main Routine
sync.Once
Once ทำให้ฟังก์ชันที่ระบุรันแค่ครั้งเดียว แม้จะถูกเรียกจากหลาย goroutine
ตัวอย่าง
package main
import (
"fmt"
"sync"
)
func setup() {
fmt.Println("Setup")
}
func main() {
var once sync.Once
var wg sync.WaitGroup
for i := 0; i // (โค้ดตัวอย่างนี้ยังไม่สมบูรณ์ในต้นฉบับ)
}
หมายเหตุ: ใน Go เรามักหลีกเลี่ยงการแชร์ตัวแปรกลางและแก้ไขค่าร่วมกันโดยตรง แทนที่จะใช้ channel ซึ่งจะอธิบายในบทความถัดไป.