การจัดการ Goroutine

Goroutine เป็นกระบวนการที่มีน้ำหนักเบา แต่ไม่ได้ฟรี: อย่างน้อยแล้ว พวกมันกินหน่วยความจำสำหรับ stack และการจัดตาราง CPU แม้ว่าค่าใช้จ่ายเหล่านี้น้อยเมื่อใช้ Goroutines อย่างไรก็ตาม การสร้างมันในจำนวนมากๆ โดยไม่มีการควบคุมชีวิตจริงๆ อาจเสี่ยงทำให้เกิดปัญหาด้านประสิทธิภาพ ยอด Goroutines ที่ไม่มีการควบคุมอาจทำให้เกิดปัญหาอื่นๆ เช่น ขัดขวางวัตถุที่ไม่ได้ใช้ทำให้ไม่มีการเก็บขยะและเชื่อมต่อทรัพยากรที่ไม่ได้ใช้

ดังนั้น อย่างของ JSON ที่ย่อย Goroutines ในโค้ดของคุณ ใช้ go.uber.org/goleak เพื่อทดสอบการรั่ว Goroutine ในแพ็คเกจที่อาจสร้าง Goroutine

โดยทั่วไป แต่ละ Goroutine ต้อง:

  • มีเวลาในการหยุดที่สามารถทำนายได้; หรือ
  • มีวิธีในการส่งสัญญาณให้ Goroutine ว่าควรหยุด

ในทั้งสองกรณี ต้องมีวิธีในการบล็อกโค้ดและรอให้ Goroutine ทำงานเสร็จสิ้น

ตัวอย่างเช่น:

ไม่แนะนำ:

go func() {
  for {
    flush()
    time.Sleep(delay)
  }
}()
// ไม่มีวิธีให้อยู่สินะ Goroutine นี้ มันกำลังทำงานไปตลอด โจทย์จะวิ่งไปตลอดจนกว่าแอปพลิเคชันจะปิด

แนะนำ:

var (
  stop = make(chan struct{}) // ส่งสัญญาณให้ Goroutine หยุด
  done = make(chan struct{}) // ส่งสัญญาณให้ Goroutine ออก
)
go func() {
  defer close(done)
  ticker := time.NewTicker(delay)
  defer ticker.Stop()
  for {
    select {
    case <-ticker.C:
      flush()
    case <-stop:
      return
    }
  }
}()

// ... โค้ดอื่นๆ
close(stop)  // บ่งชี้ให้ Goroutine หยุด
<-done       // และรอให้มันออก

// สามารถรอ Goroutine ด้วย close(stop) และเราสามารถรอให้มันออก <-done

การรอให้ Goroutines ออก

เมื่อมี Goroutine ที่สร้างขึ้นโดยระบบ จะต้องมีวิธีการรอให้ Goroutine ออก มีวิธีการที่สองที่สามารถทำได้:

  • การใช้ sync.WaitGroup ใช้วิธีนี้เมื่อต้องรอหลาย Goroutines
var wg sync.WaitGroup
for i := 0; i < num; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        // ...
    }()
}
wg.Wait()
  • เพิ่ม chan struct{} อีกชุดและทำการปิดหลังจาก Goroutine เสร็จสิ้น ใช้วิธีนี้เมื่อมีแค่ Goroutine เดียว
done := make(chan struct{})
go func() {
    defer close(done)
    // ...
}()
// เพื่อรอให้ Goroutine เสร็จสิ้น:
<-done

init() ฟังก์ชันไม่ควรสร้าง Goroutines อย่างไรก็ตาม อ่านเพิ่มเติมที่ หลีกเลี่ยงการใช้ init()

หากแพ็คเกจต้องการ Goroutine แบ็คกราวด์ จะต้องแสดงวัตถุที่รับผิดชอบในการจัดการชีวิตการทำงานของ Goroutine วัตถุนี้ต้องมีวิธี (ปิด, หยุด, ปิดเสียง ฯลฯ) เพื่อบ่งบอกถึงการสิ้นสุดของ Goroutine แบ็คกราวด์และรอให้มันออก

ไม่แนะนำ:

func init() {
    go doWork()
}
func doWork() {
    for {
        // ...
    }
}
// เมื่อผู้ใช้ส่งออกแพ็กเกจนี้ จะสร้าง Goroutine แบ็คกราวด์โดยไม่เงียบงันได้ ผู้ใช้ไม่สามารถควบคุม Goroutine หรือหยุดมัน

แนะนำ:

type Worker struct{ /* ... */ }
func NewWorker(...) *Worker {
    w := &Worker{
        stop: make(chan struct{}),
        done: make(chan struct{}),
        // ...
    }
    go w.doWork()
}
func (w *Worker) doWork() {
    defer close(w.done)
    for {
        // ...
        select {
        case <-w.stop:
            return
        }
    }
}
// ปิดบอกผู้ใช้ให้หยุ Goroutine
// และรอให้มันเสร็จสิ้น
func (w *Worker) Shutdown() {
    close(w.stop)
    <-w.done
}

สร้าง Worker เมื่อมีคำขอจากผู้ใช้เท่านั้น และมีเมธอดให้ปิด Worker เพื่อให้ผู้ใช้ปล่อยทรัพยากรที่ใช้โดย Worker

โปรดทราบว่าหาก Worker จัดการกับ Goroutines หลายๆ ตัว ควรใช้ WaitGroup