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: