Quản lý Goroutine

Goroutines là nhẹ nhưng chúng không phải là miễn phí: ít nhất, chúng tiêu tốn bộ nhớ cho ngăn xếp và lập lịch CPU. Mặc dù những chi phí này nhỏ đối với việc sử dụng Goroutines, tạo chúng ở số lượng lớn mà không có một vòng đời được kiểm soát có thể dẫn đến những vấn đề về hiệu suất nghiêm trọng. Goroutines với vòng đời không được quản lý cũng có thể dẫn đến những vấn đề khác, như ngăn chặn các đối tượng không sử dụng khỏi được thu gom rác và giữ lại các tài nguyên không sử dụng.

Vì vậy, không được rò rỉ goroutines trong mã code của bạn. Sử dụng go.uber.org/goleak để kiểm tra rò rỉ goroutine trong một gói có thể tạo ra goroutines.

Về cơ bản, mỗi goroutine phải:

  • Có thời gian dừng dự đoán; hoặc
  • Có cách để thông báo cho goroutine rằng nó nên dừng lại.

Trong cả hai trường hợp, phải có cách để mã code chặn và chờ đợi goroutine hoàn thành.

Ví dụ:

Không khuyến nghị:

go func() {
  for {
    flush()
    time.Sleep(delay)
  }
}()
// Không có cách nào để dừng goroutine này. Nó sẽ chạy mãi mãi cho đến khi ứng dụng kết thúc.

Khuyến nghị:

var (
  stop = make(chan struct{}) // Thông báo cho goroutine dừng lại
  done = make(chan struct{}) // Thông báo rằng goroutine đã thoát
)
go func() {
  defer close(done)
  ticker := time.NewTicker(delay)
  defer ticker.Stop()
  for {
    select {
    case <-ticker.C:
      flush()
    case <-stop:
      return
    }
  }
}()

// ... mã code khác
close(stop)  // Chỉ định rằng goroutine nên dừng lại
<-done       // và chờ đợi nó thoát

// Goroutine này có thể được chờ đợi với close(stop), và ta có thể chờ đợi nó thoát <-done.

Chờ đợi Goroutines thoát

Khi có một goroutine được tạo bởi hệ thống, phải có cách để đợi goroutine thoát. Có hai phương pháp thông thường để đạt được điều này:

  • Sử dụng sync.WaitGroup. Sử dụng phương pháp này khi chờ đợi nhiều goroutines.
var wg sync.WaitGroup
for i := 0; i < num; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        // ...
    }()
}
wg.Wait()
  • Thêm một chan struct{} khác và đóng nó sau khi goroutine hoàn thành. Sử dụng phương pháp này nếu chỉ có một goroutine.
done := make(chan struct{})
go func() {
    defer close(done)
    // ...
}()
// Để chờ goroutine hoàn tất:
<-done

Hàm init() không nên tạo goroutines. Hơn nữa, xem thêm Tránh sử dụng init().

Nếu một gói yêu cầu một goroutine nền, nó phải tiết lộ một đối tượng chịu trách nhiệm quản lý vòng đời của goroutine. Đối tượng này phải cung cấp một phương thức (Close, Stop, Shutdown, v.v.) để chỉ ra việc kết thúc của goroutine nền và chờ đợi nó thoát.

Không khuyến nghị:

func init() {
    go doWork()
}
func doWork() {
    for {
        // ...
    }
}
// Khi người dùng xuất gói này, một goroutine nền được tạo mà không kiểm soát. Người dùng không thể kiểm soát goroutine hoặc dừng nó.

Khuyến nghị:

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
        }
    }
}
// Shutdown cho worker dừng lại
// và chờ đợi nó hoàn tất.
func (w *Worker) Shutdown() {
    close(w.stop)
    <-w.done
}

Tạo worker chỉ khi được yêu cầu bởi người dùng và cung cấp một phương thức để tắt worker để người dùng có thể giải phóng tài nguyên được sử dụng bởi worker.

Lưu ý rằng nếu worker quản lý nhiều goroutines, nên sử dụng WaitGroup.