Go Concurrency
Back to Go Index
Goroutine
- Go is a concurrent language
- keyword คือ
goวางไว้หน้าฟังก์ชัน เช่นgo doSomeThing() - Goroutine คือตัวภาษา Go จะทำการสร้าง layer บาง ๆ ขึ้นมา ก่อนจะไปจัดการเรื่อง thread จริง ๆ ใน OS โดยจะมี scheduling ของตนเอง จัดคิวเอง ใช้ทรัพยากรน้อยเมื่อเทียบกับการทำ thread จริง ๆ (มองว่าเป็น lightweight thread ได้)
Basic Example
ตัวอย่างเปรียบเทียบแบบใช้และไม่ใช้ Goroutine จะเห็นว่าถ้าใช้ Goroutine งานจะเสร็จเร็วกว่าเพราะไม่ต้องรอกัน
func doSomething() {
time.Sleep(100 * time.Millisecond)
fmt.Println("Doing something")
}รันแบบปกติแล้วจับเวลา
func main() {
start := time.Now()
doSomething()
doSomething()
doSomething()
time.Sleep(500 * time.Millisecond)
fmt.Println(time.Since(start)) // 842.2191ms
}รันแบบ goroutine แล้วจับเวลา ซึ่งพบว่าเร็วกว่าเพราะไม่มีการรอทำตามลำดับ
func main() {
start := time.Now()
go doSomething()
go doSomething()
go doSomething()
time.Sleep(500 * time.Millisecond)
fmt.Println(time.Since(start)) // 514.1011ms
}Graceful Shutdown
- ในการทำงานจริงของ Goroutine จะมีการทำงานพร้อมกันมาก ๆ ในเวลาเดียวกัน
- ข้อควรระวัง คือ ถ้า main goroutine หยุดการทำงาน (เช่น update service หรือหยุดดื้อ ๆ ) พวก goroutine ต่าง ๆ ที่ถูกปล่อยออกมาก็จะหยุดตามไปด้วย งานที่ทำไว้ยังไม่เสร็จก็หยุด
- ดังนั้นจึงต้องมีการคำนึงถึง Graceful shutdown เพื่อให้จัดการเรื่องเหล่านี้ ซึ่งอาจเขียนไว้ใน document ของ library ต่าง ๆ
Waitgroup
- เป็นวิธีการรอของจาก Goroutine กลับมา
- waitgroup อยู่ใน package sync
- การใช้คือ ปกติแล้ว main จะไม่รู้ว่า Goroutine แต่ละตัวทำเสร็จหรือเปล่า เลยต้อง sleep รอเผื่อไว้มาก ๆ
- การใช้ waitgroup จะทำให้เรารู้เวลาที่แต่ละตัวทำเสร็จจริง ๆ
Waitgroup Example
func doSomething(wg *sync.WaitGroup) { // แก้ doSomething ให้รับค่า parameter wg *sync.WaitGroup()
time.Sleep(100 * time.Millisecond)
fmt.Println("Doing something")
wg.Done() // ในฟังก์ชันให้เพิ่ม wg.Done() เพื่อบอกว่างานเสร็จแล้ว
}
func main() {
wg := &sync.WaitGroup{} // ประกาศ pointer &sync.WaitGroup() มาใส่ใน wg เพราะต้องมีการเปลี่ยน state
wg.Add(3) // เรารู้จำนวน Goroutine ที่แน่ชัดว่ามีเท่าไร ใส่ 3 โดยเปรียบเสมือน counter อันนึง
start := time.Now()
// ส่ง wg เข้าไปในฟังก์ชันทุกตัว
go doSomething(wg)
go doSomething(wg)
go doSomething(wg)
wg.Wait() // ใส่ wg.Wait() แทนที่ Sleep
fmt.Println(time.Since(start))
}Race Condition
- ในการทำงานจริงกับ Concurrency ต้องระวังเรื่อง Race Condition
- Race Condition คือการที่มี Goroutine มากกว่า 1 ตัว พยายามที่จะมา Access ตัวแปรตัวเดียวกัน
- กรณีนี้เราสามารถ Detect ได้ เกิดเป็น error warning ว่า data race อาจเกิดขึ้นได้ที่ไหน
Channel
- Channel เปรียบเสมือนช่องทางที่ใช้สื่อสารกับ Goroutine เช่น Main Goroutine กับ Goroutine หรือ Goroutine กับ Goroutine
- ความเป็นจริงมันคือการแชร์ memory ไว้ที่นึง
- Channel ก่อนต้องใช้ Make ก่อน เหมือนกับ Slice และ Map พอเริ่มต้นจะเป็น nil
- มี keyword คือ
chan
Channel มี 2 แบบ ได้แก่:
Unbuffered Channel
- การันตี synchronization
- ถ้ามีคนพยายามส่งของ แต่ไม่มีคนมารับ ของมันจะไม่ไหลไปทางฝั่งคนรับ ของจะค้างอยู่ฝั่งคนส่ง
Buffered Channel
- มีคนส่งของ ของนั้นจะมายังฝั่งคนรับได้ แม้จะยังไม่มีคนมารับก็ตาม โดยของจะเก็บได้ตามจำนวน buffer ถ้าเต็มแล้วของก็จะส่งมาไม่ได้ จนกว่าของใน buffer จะถูกหยิบไปใช้
- ทำให้มีโอกาสที่ของจะหายได้ จึงไม่ควรใช้กับของที่มีความสำคัญมาก
2-Way Channel
- Channel ปกติที่ประกาศด้วย chan type จะเป็น 2-way Channel คือสามารถใช้ได้ทั้ง read/write
- เวลาเอาของใส่เข้าไปให้ใช้ arrow (←) ถ้าเอาของออกก็ชี้ออก
func demo() {
rw := make(chan string)
go greeting(rw)
rw <- "Nattrio"
fmt.Println(<-rw)
}
func greeting(rw chan string) {
s := <-rw
rw <- "Hello " + s
}
func main() {
demo() // Hello Nattrio
}1-Way Channel
- Channel ที่สามารถกำหนดทิศทางได้ โดยใส่ arrow ตอนประกาศ
demoOneWay()เป็น read-only แปลว่าของที่คืนมา ใช้อ่านได้อย่างเดียว ไม่สามารถ write ของเข้าไปได้demoWriteChannel()เป็น write-only แปลว่าใช้ write ของเข้าไปได้อย่างเดียว ไม่สามารถ read ออกมาได้
func demoOneWay() <-chan string {
ch := make(chan string)
go demoWriteChannel(ch, "Nattrio")
return ch
}
func demoWriteChannel(w chan<- string, name string) {
w <- "Hello " + name
}Range with Channel
- เวลารับของจาก for range จะได้แค่ค่าเดียว คือค่าที่ไหลเข้ามาใน Channel
- โดยจะรับค่าไปจนกว่า Channel จะถูก close
- (ปกติ Channel ใน Go ไม่จำเป็นต้องไป close มัน เราจะ close ก็ต่อเมื่อต้องการส่ง signal เพื่อบอกว่าไม่มีของแล้ว โดยสามารถ close ได้ ทั้งจากฝั่งรับและฝั่งส่ง)
- (ต้องระวัง ถ้าเกิดไปส่ง close ซ้ำกันมันจะเกิด panic)
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
}Select Statement
- กรณีที่มี Goroutine หรือ Channel มากกว่าหนึ่งตัวทำงาน แล้วเราไม่ต้องการจะรอทุกตัว คือ เอาแค่ตัวใดตัวหนึ่ง ตัวไหนมาแล้วก็ไม่ให้มัน block จังหวะนั้น
- เพราะทุกครั้งที่รอ Channel มันจะ Block ในบรรทัดนั้น
- Select Statement จะเข้ามาช่วยไม่ให้มัน Block
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}Related: