Skip to main content

รู้จักกับ Sync package

Sync package คืออะไร

Ref: https://pkg.go.dev/sync

Sync package ในภาษา Go เป็น package มาตรฐานที่ให้บริการฟังก์ชันและโครงสร้างข้อมูลสำหรับการประสานงานระหว่าง goroutines ฟังก์ชันและโครงสร้างข้อมูลเหล่านี้ช่วยให้นักพัฒนาสามารถเขียนโปรแกรมที่มี goroutines จำนวนมากที่ทำงานร่วมกันได้อย่างมีประสิทธิภาพและหลีกเลี่ยงปัญหาที่อาจเกิดขึ้น เช่น deadlocks, race condition ได้

เพิ่มเติม

  • Race condition คือ ปัญหาที่อาจเกิดขึ้นเมื่อหลาย goroutine พยายามเข้าถึงและแก้ไขทรัพยากรหรือข้อมูลร่วมกันในเวลาเดียวกัน ปัญหานี้อาจทำให้ข้อมูลหรือผลลัพธ์ของโปรแกรมไม่ถูกต้องหรือคาดเดาไม่ได้

ฟังก์ชันและโครงสร้างข้อมูลที่สำคัญบางประการใน Sync package ได้แก่:

  • Mutex ช่วยให้ goroutines สามารถเข้าถึงทรัพยากรร่วมกันได้อย่างปลอดภัย Mutex ทำงานโดยการล็อกทรัพยากรเมื่อ goroutine กำลังใช้งานอยู่ และปลดล็อกทรัพยากรเมื่อ goroutine เสร็จสิ้นการใช้งาน
var mu sync.Mutex
mu.Lock()
// critical section
mu.Unlock()
  • Cond ช่วยให้ goroutines รอเงื่อนไขบางอย่างให้เป็นจริง Cond ทำงานโดยการล็อกทรัพยากรเมื่อ goroutine กำลังรอ และปลดล็อกทรัพยากรเมื่อเงื่อนไขเป็นจริง
var mu sync.Mutex
cond := sync.NewCond(&mu)
// goroutine waits on condition
cond.Wait()
// signal or broadcast to wake goroutine
cond.Signal() // or cond.Broadcast()

var rwmu sync.RWMutex
rwmu.RLock() // for reading
// read section
rwmu.RUnlock()

rwmu.Lock() // for writing
// write section
rwmu.Unlock()
  • Once sync.Once เป็นการ synchronization แบบดั้งเดิม ที่ package sync จัดเตรียมไว้ เพื่อให้แน่ใจว่า code บางส่วนจะถูกเรียกใช้งานเพียง "ครั้งเดียว" โดยไม่คำนึงว่า goroutine จะ run code ไปแล้วกี่ครั้งก็ตาม
var once sync.Once
once.Do(func() {
// initialization or setup code
})
  • WaitGroup ช่วยให้ goroutines รอให้ goroutines อื่น ๆ ทำงานเสร็จสิ้น WaitGroup ทำงานโดยการนับจำนวน goroutines ที่ทำงานอยู่ เมื่อจำนวน goroutines เหลือศูนย์ ฟังก์ชัน Wait() จะหยุดการทำงาน
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// do work
}()
wg.Wait()

WaitGroup

WaitGroup เป็นโครงสร้างข้อมูลแบบ mutex-guarded counter ที่ช่วยให้ goroutines สามารถรอให้ goroutines อื่น ๆ ทำงานเสร็จสิ้น WaitGroup ทำงานโดยการนับจำนวน goroutines ที่ทำงานอยู่ เมื่อจำนวน goroutines เหลือศูนย์ ฟังก์ชัน Wait() จะหยุดการทำงาน

ประโยชน์

  • ช่วยให้ goroutines สามารถรอให้ goroutines อื่น ๆ ทำงานเสร็จสิ้นได้อย่างมีประสิทธิภาพ
  • ปรับปรุงประสิทธิภาพโดยหลีกเลี่ยงการวนซ้ำตรวจสอบสถานะของ goroutines อื่น ๆ อย่างต่อเนื่อง
package main

import (
"fmt"
"math/rand"
"sync"
"time"
)

func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Decrement the counter when the goroutine completes

fmt.Printf("Worker %d starting\n", id)

// Simulate some work by sleeping
sleepDuration := time.Duration(rand.Intn(1000)) * time.Millisecond
time.Sleep(sleepDuration)

fmt.Printf("Worker %d done\n", id)
}

func main() {
var wg sync.WaitGroup

// Launch several goroutines and increment the WaitGroup counter for each
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}

wg.Wait() // Block until the WaitGroup counter goes back to 0; all workers are done

fmt.Println("All workers completed")
}

ตัวอย่างการใช้งาน

  • การรอให้ goroutines ทั้งหมดทำงานเสร็จสิ้น เช่น การรวบรวมผลลัพธ์จาก goroutines หลายตัว
  • การประสานงานระหว่าง goroutines เช่น การรอให้ goroutine อื่นเริ่มต้นทำงาน

Mutex

Mutex เป็นโครงสร้างข้อมูลแบบ mutex-guarded lock ที่ช่วยให้ goroutines สามารถเข้าถึงทรัพยากรร่วมกันได้อย่างปลอดภัย Mutex ทำงานโดยการล็อกทรัพยากรเมื่อ goroutine กำลังใช้งานอยู่ และปลดล็อกทรัพยากรเมื่อ goroutine เสร็จสิ้นการใช้งาน

ประโยชน์

  • ป้องกัน race conditions
  • ช่วยให้ goroutines ทำงานร่วมกันได้อย่างปลอดภัย ปรับปรุงประสิทธิภาพโดยหลีกเลี่ยงการเข้าถึงทรัพยากรร่วมกันโดยไม่จำเป็น

เริ่มจาก code เล็กๆกันก่อน

package main

import (
"fmt"
"sync"
"time"
)

var m sync.Mutex

var n = 10

func p() {
m.Lock()
fmt.Println("LOCK")
fmt.Println(n)
time.Sleep(1 * time.Second)
m.Unlock()
fmt.Println("UNLOCK")
}

func main() {
fmt.Println("FIRST")
go p()
fmt.Println("SECOND")
p()
fmt.Println("THIRD")
time.Sleep(3 * time.Second)
fmt.Println("DONE")
}

Note

  • mutex.Lock() เป็นคำสั่งสำหรับการ Lock resource และจะรอจนกว่าการปลดล็อค ( mutex.Unlock()) จะเกิดขึ้น

มาลองประยุกต์ใช้กับเคส Counter กัน

package main

import (
"fmt"
"sync"
)

// Counter struct holds a value and a mutex
type Counter struct {
value int
mu sync.Mutex
}

// Increment method increments the counter's value safely using the mutex
func (c *Counter) Increment() {
c.mu.Lock() // Lock the mutex before accessing the value
c.value++ // Increment the value
c.mu.Unlock() // Unlock the mutex after accessing the value
}

// Value method returns the current value of the counter
func (c *Counter) Value() int {
return c.value
}

func main() {
var wg sync.WaitGroup
counter := Counter{}

// Start 10 goroutines
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
counter.Increment()
}
}()
}

wg.Wait() // Wait for all goroutines to finish
fmt.Println("Final counter value:", counter.Value())
}

ตัวอย่างการใช้งาน

  • การป้องกันการใช้ Resource ร่วมกัน เช่น ไฟล์ ฐานข้อมูล หรือโครงสร้างข้อมูล
  • การประสานงานระหว่าง goroutines เช่น การเข้าแถวรอทรัพยากรหรือการสื่อสารระหว่าง goroutines

Once

Once เป็นโครงสร้างข้อมูลแบบ mutex-guarded conditional variable ที่ช่วยให้ goroutines สามารถดำเนินการเฉพาะได้ครั้งเดียวเท่านั้น

ประโยชน์

  • ช่วยให้ goroutines สามารถดำเนินการเฉพาะได้ครั้งเดียวเท่านั้น
  • ป้องกัน race conditions จากคำสั่งที่ต้องทำเพียงแค่ครั้งเดียวในระบบ
package main

import (
"fmt"
"sync"
)

func main() {
var once sync.Once
var wg sync.WaitGroup

initialize := func() {
fmt.Println("Initializing only once")
}

doWork := func(workerId int) {
defer wg.Done()
fmt.Printf("Worker %d started\n", workerId)
once.Do(initialize) // This will only be executed once
fmt.Printf("Worker %d done\n", workerId)
}

numWorkers := 5
wg.Add(numWorkers)

// Launch several goroutines
for i := 0; i < numWorkers; i++ {
go doWork(i)
}

// Wait for all goroutines to complete
wg.Wait()
fmt.Println("All workers completed")
}

ตัวอย่างการใช้งาน

  • การเปิดการเชื่อมต่อฐานข้อมูล
  • การเริ่มต้นบริการ

Cond

Cond เป็นโครงสร้างข้อมูลแบบ mutex-guarded condition variable ที่ช่วยให้ goroutines สามารถรอเงื่อนไขบางอย่างให้เป็นจริง Cond ทำงานโดยการล็อกทรัพยากรเมื่อ goroutine กำลังรอ และปลดล็อกทรัพยากรเมื่อเงื่อนไขเป็นจริง

ประโยชน์

  • ช่วยให้ goroutines สามารถรอเหตุการณ์บางอย่างได้อย่างมีประสิทธิภาพ
  • ปรับปรุงประสิทธิภาพโดยหลีกเลี่ยงการวนซ้ำตรวจสอบเงื่อนไขอย่างต่อเนื่อง
package main

import (
"fmt"
"sync"
"time"
)

func main() {
// Create a new condition variable
var mutex sync.Mutex
cond := sync.NewCond(&mutex)

// A shared resource
ready := false

// A goroutine that waits for a condition
go func() {
fmt.Println("Goroutine: Waiting for the condition...")

mutex.Lock()
for !ready {
cond.Wait() // Wait for the condition
}
fmt.Println("Goroutine: Condition met, proceeding...")
mutex.Unlock()
}()

// Simulate some work (e.g., loading resources)
time.Sleep(2 * time.Second)

// Signal the condition
mutex.Lock()
ready = true
cond.Signal() // Signal one waiting goroutine
mutex.Unlock()
fmt.Println("Push signal !")

// Give some time for the goroutine to complete
time.Sleep(1 * time.Second)
fmt.Println("Main: Work is done.")
}

Note

  • Cond กับ Mutex ต่างก็เป็นตัวแปรแบบ sync.Locker ทั้งคู่ แต่ทำหน้าที่ต่างกัน

  • Mutex ทำหน้าที่ป้องกันไม่ให้ goroutine อื่นเข้าถึงทรัพยากรร่วมกันในเวลาเดียวกัน

  • ขณะที่ Cond ทำหน้าที่เป็นสัญญาณระหว่าง goroutine ต่างๆ

  • Cond ทำงานโดยอาศัย mutex เป็นตัวล็อกทรัพยากรร่วมกัน เมื่อ goroutine ต้องการที่จะส่งสัญญาณให้ goroutine อื่น จะต้องทำการ Lock mutex ก่อน จากนั้นจึงเรียกใช้ฟังก์ชัน Signal() หรือ Broadcast() เมื่อ goroutine อื่นทำการปลดล็อค mutex ก็จะสามารถตรวจจับสัญญาณได้

  • ดังนั้น Cond จึงจำเป็นต้องใช้งานคู่กับ Mutex เพื่อให้สามารถทำงานได้อย่างมีประสิทธิภาพ Mutex จะทำหน้าที่ป้องกันไม่ให้ goroutine อื่นเข้ามารบกวนการทำงานของ goroutine ที่ส่งสัญญาณ ในขณะที่ Cond จะทำหน้าที่ส่งสัญญาณให้ goroutine อื่นรับรู้ถึงเหตุการณ์ที่ต้องการ

ตัวอย่างการใช้งาน

  • การรอเหตุการณ์บางอย่าง เช่น ข้อความจากเครือข่ายหรือความพร้อมใช้งานของทรัพยากร
  • การประสานงานระหว่าง goroutines เช่น การรอให้ goroutine อื่นทำงานเสร็จสิ้น

Mutex-guarded condition variable

mutex-guarded condition variable เป็นโครงสร้างข้อมูลแบบ mutex-guarded ที่ช่วย goroutines สามารถรอเงื่อนไขบางอย่างให้เป็นจริง Mutex-guarded condition variable ประกอบด้วยสองส่วนหลัก:

  • Mutex = ช่วยให้ goroutines สามารถเข้าถึงทรัพยากรร่วมกันได้อย่างปลอดภัย
  • Condition variable = ช่วยให้ goroutines สามารถรอเงื่อนไขบางอย่างให้เป็นจริง

การทำงานพื้นฐานของ mutex-guarded condition variable เป็นไปตามขั้นตอนต่อไปนี้:

  1. Goroutine ที่ต้องการรอเงื่อนไขบางอย่างจะล็อก mutex ที่เกี่ยวข้อง
  2. Goroutine จะเรียกฟังก์ชัน wait() ของ condition variable ซึ่งจะปลดล็อก mutex และบล็อก goroutine นั้น
  3. เมื่อเงื่อนไขเป็นจริง goroutine อื่น ๆ ใด ๆ ก็สามารถเรียกฟังก์ชัน signal() หรือ broadcast() ของ condition variable ได้
  4. ฟังก์ชัน signal() จะปลุก goroutine หนึ่งตัวที่กำลังรอใน condition variable
  5. ฟังก์ชัน broadcast() จะปลุก goroutine ทั้งหมดที่กำลังรอใน condition variable

เมื่อ goroutine ที่รอเงื่อนไขบางอย่างถูกปลุกขึ้นมา มันจะปลดล็อก mutex และดำเนินการต่อไป

mutex-guarded condition variable มักใช้เพื่อประสานงานระหว่าง goroutines เช่น การรอให้เหตุการณ์บางอย่างเกิดขึ้น การรอให้ goroutine อื่นทำงานเสร็จสิ้น หรือการป้องกัน race conditions

ตัวอย่างการใช้งาน mutex-guarded condition variable ได้แก่:

  • การรอข้อความจาก Network = Goroutine ที่ต้องการรับข้อความจากเครือข่ายสามารถรอใน condition variable จนกระทั่งมีข้อความมาถึง
  • การรอให้ goroutine อื่นทำงานเสร็จสิ้น = Goroutine ที่ต้องการรอให้ goroutine อื่นทำงานเสร็จสิ้นสามารถรอใน condition variable จนกระทั่ง goroutine อื่นนั้นเสร็จสิ้นการทำงาน
  • การป้องกัน race conditions = Goroutine ที่ต้องการป้องกัน race conditions สามารถรอใน condition variable จนกระทั่งทรัพยากรที่เกี่ยวข้องพร้อมใช้งาน

เพิ่มเติมปัญหา Deadlock

Deadlock สำหรับ goroutine หมายถึงสถานการณ์ที่ goroutine จะอยู่สถานะ block "ตลอดกาล" โดย

  • ทำการรอ Resource อื่นมาปล่อย block
  • แต่ไม่มี Resource ตัวไหนมาทำการปล่อยให้

สถานการณ์นี้มักเกิดจากการจัดการ Concurrency ไว้ไม่ถูกต้องจากตัวของ channel, mutexes หรือ waitgroup โดยสถานที่มักจะเกิด deadlock ขึ้นได้ประจำจะมีดังนี้

  1. Mutual Locking (Mutex Deadlock)

Gouroutine ตั้งแต่สองตัวขึ้นไปต่างก็ถือ mutex และรอรับ mutex ที่อีกตัวถืออยู่ (อยู่ wait state ทั้งคู่) สิ่งนี้จะสร้างวงจรการขึ้นต่อกันที่ไม่สามารถแก้ไขได้ ซึ่งนำไปสู่ Deadlock

  1. Channel deadlock

สิ่งนี้เกิดขึ้นเมื่อ goroutine กำลังรอส่งหรือรับข้อมูลใน channel แต่ไม่มี goroutine อื่นที่พร้อมให้ดำเนินการตรงกันข้าม (receive / send ข้อมูลใน channel) ตัวอย่างเช่น

  • Deadlock อาจเกิดขึ้นได้หาก goroutine ทั้งหมดกำลังรอรับจากช่อง แต่ไม่มี goroutine ใดถูกส่งไป (หรือกลับกัน มีคนส่งไปแต่ไม่มีคนรับ)
  1. Improper Use of WaitGroups

Deadlock อาจเกิดขึ้นได้หากใช้ sync.WaitGroup ไม่ถูกต้อง ตัวอย่างเช่น

  • รอบน WaitGroup โดยไม่ได้รับประกันว่าการดำเนินการเพิ่มทั้งหมดจะเสร็จสิ้นก่อนการรอ
  • หากจำนวน Done ไม่ตรงกับจำนวนการ Add ที่เพิ่มไป = เข้าสู่ภาวะ Deadlock ได้ เนื่องจากเป็นการรอไม่มีที่สิ้นสุด
  1. Nested Locks

Deadlock ยังอาจเกิดขึ้นได้เมื่อคุณมีการล็อกแบบซ้อนกัน (การรับ mutexes หลายตัว) โดยไม่มีลำดับการล็อกที่สอดคล้องกัน ซึ่งนำไปสู่เงื่อนไขการรอแบบวงกลม (circular wait conditions)

Deadlock อาจเกิดขึ้นเล็กน้อยและมักจะ debug ได้ยาก เนื่องจากอาจเกิดขึ้นภายใต้เงื่อนไขเฉพาะหรือในช่วงเวลาที่เจาะจงเท่านั้น

เพื่อหลีกเลี่ยง Deadlock สิ่งสำคัญคือต้องออกแบบรูปแบบการ interaction goroutine อย่างระมัดระวัง หลีกเลี่ยงการ lock goroutine หลายรายการ (ใช้เท่าที่จำเป็น) และคำนึงถึงวิธีที่ goroutine สื่อสารและ synchronize ระหว่างกันด้วย