Manajemen Goroutine

Goroutine adalah ringan, namun tidak gratis: setidaknya, mereka menggunakan memori untuk tumpukan dan penjadwalan CPU. Meskipun biaya ini kecil untuk penggunaan Goroutine, menghasilkan mereka dalam jumlah besar tanpa siklus hidup yang terkendali dapat menyebabkan masalah kinerja yang serius. Goroutine dengan siklus hidup yang tidak terkelola juga dapat menyebabkan masalah lain, seperti mencegah objek yang tidak terpakai dari dikumpulkan sebagai sampah dan menyimpan sumber daya yang tidak terpakai.

Oleh karena itu, jangan bocorkan goroutine dalam kode Anda. Gunakan go.uber.org/goleak untuk menguji kebocoran goroutine dalam paket yang mungkin menghasilkan goroutine.

Secara umum, setiap goroutine harus:

  • Memiliki waktu berhenti yang dapat diprediksi; atau
  • Memiliki cara untuk memberi sinyal kepada goroutine bahwa itu harus berhenti.

Dalam kedua kasus tersebut, harus ada cara bagi kode untuk memblokir dan menunggu goroutine untuk selesai.

Sebagai contoh:

Tidak disarankan:

go func() {
  for {
    flush()
    time.Sleep(delay)
  }
}()
// Tidak ada cara untuk menghentikan goroutine ini. Itu akan berjalan tanpa batas sampai aplikasi keluar.

Disarankan:

var (
  stop = make(chan struct{}) // Sinyalkan goroutine untuk berhenti
  done = make(chan struct{}) // Sinyalkan bahwa goroutine telah keluar
)
go func() {
  defer close(done)
  ticker := time.NewTicker(delay)
  defer ticker.Stop()
  for {
    select {
    case <-ticker.C:
      flush()
    case <-stop:
      return
    }
  }
}()

// ... kode lainnya
close(stop)  // Tunjukkan bahwa goroutine harus berhenti
<-done       // dan tunggu sampai itu keluar

// Goroutine ini dapat ditunggu dengan close(stop), dan kita dapat menunggu sampai keluar dengan <-done.

Menunggu Goroutine untuk Keluar

Ketika diberikan sebuah goroutine yang dihasilkan oleh sistem, harus ada cara untuk menunggu goroutine untuk keluar. Ada dua metode umum untuk mencapai hal ini:

  • Menggunakan sync.WaitGroup. Gunakan metode ini ketika menunggu untuk beberapa goroutine.
var wg sync.WaitGroup
for i := 0; i < num; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        // ...
    }()
}
wg.Wait()
  • Menambahkan chan struct{} lain, dan menutupnya setelah goroutine selesai. Gunakan metode ini jika hanya ada satu goroutine.
done := make(chan struct{})
go func() {
    defer close(done)
    // ...
}()
// Untuk menunggu goroutine selesai:
<-done

Fungsi init() tidak boleh membuat goroutine. Selain itu, lihat Hindari menggunakan init().

Jika suatu paket memerlukan goroutine latar belakang, itu harus mengekspos objek yang bertanggung jawab untuk mengelola siklus hidup goroutine. Objek ini harus menyediakan metode (Close, Stop, Shutdown, dll.) untuk menunjukkan terminasi goroutine latar belakang dan menunggu keluar.

Tidak Disarankan Pendekatan:

func init() {
    go doWork()
}
func doWork() {
    for {
        // ...
    }
}
// Ketika pengguna mengekspor paket ini, goroutine latar belakang dihasilkan tanpa syarat. Pengguna tidak dapat mengontrol goroutine atau menghentikannya.

Pendekatan yang Disarankan:

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 memberitahu pekerja untuk berhenti
// dan menunggu sampai selesai.
func (w *Worker) Shutdown() {
    close(w.stop)
    <-w.done
}

Hasilkan pekerja hanya ketika diminta oleh pengguna, dan berikan metode untuk mematikan pekerja sehingga pengguna dapat melepaskan sumber daya yang digunakan oleh pekerja.

Perhatikan bahwa jika pekerja mengelola beberapa goroutine, WaitGroup harus digunakan.