Goroutine: Performa & Tradeoffs
Memahami batasan performa goroutine β contention, overhead, resource limits, dan cara mengoptimalkannya.
Versi Bahasa Indonesia. Baca dalam Bahasa Inggris
Menambah lebih banyak goroutine tidak akan selalu meningkatkan performa karena beberapa batasan praktis dan inefisiensi dalam sistem konkuren. Faktor-faktor ini termasuk contention, overhead, dan keterbatasan sumber daya.
Contention
Contention terjadi ketika banyak goroutine bersaing untuk sumber daya yang sama (misalnya memori, CPU, atau I/O).
- Akses Memori Bersama: Goroutine mungkin perlu mengakses variabel atau struktur data bersama. Menggunakan primitive sinkronisasi seperti sync.Mutex atau sync.RWMutex dapat membuat akses menjadi serial, menyebabkan beberapa goroutine memblokir.
- I/O Bottlenecks: Ketika banyak goroutine membuat request jaringan atau disk I/O secara simultan, mereka bersaing untuk hardware yang mendasarinya, menciptakan contention.
- Sumber Daya CPU: Bahkan dengan banyak CPU core, jika workload bersifat CPU-bound, menambah lebih banyak goroutine tidak akan membantu melampaui jumlah core yang tersedia.
Contoh
package main
import (
"fmt"
"sync"
)
func incrementCounter(wg *sync.WaitGroup, mu *sync.Mutex, counter *int) {
defer wg.Done()
mu.Lock()
*counter++
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
counter := 0
// Luncurkan 1000 goroutine
for i := 0; i < 1000; i++ {
wg.Add(1)
go incrementCounter(&wg, &mu, &counter)
}
wg.Wait()
fmt.Println("Final Counter Value:", counter)
}Dalam contoh ini:
- Meskipun ada 1000 goroutine, contention terjadi di bagian
mu.Lock(), di mana hanya satu goroutine yang dapat memperbarui counter pada satu waktu. - Ini membatasi peningkatan performa.
Overhead
Setiap goroutine memiliki biaya kecil namun tidak nol. Menambahkan banyak goroutine dapat meningkatkan overhead untuk runtime Go.
- Penjadwalan Goroutine: Runtime Go menjadwalkan goroutine ke thread OS. Mengelola ribuan goroutine menambah overhead penjadwalan.
- Konsumsi Memori: Setiap goroutine dimulai dengan stack kecil (misalnya 2KB), yang dapat tumbuh sesuai kebutuhan. Membuat jutaan goroutine dapat menyebabkan penggunaan memori yang signifikan.
- Garbage Collection (GC): Sejumlah besar goroutine dan penggunaan memori bersama dapat meningkatkan beban kerja garbage collector.
package main
import (
"fmt"
"time"
)
func work() {
time.Sleep(2 * time.Second) // Simulasi workload
}
func main() {
for i := 0; i < 1_000_000; i++ { // Luncurkan 1 juta goroutine
go work()
}
fmt.Println("All goroutines started")
time.Sleep(5 * time.Second)
}Dalam contoh ini:
- Meskipun Go efisien, meluncurkan 1 juta goroutine dapat menyebabkan program kehabisan memori atau mengalami penundaan signifikan karena overhead penjadwalan.
Resource Limits
Menambah lebih banyak goroutine tidak dapat mengatasi batasan fisik dan logis dari sistem.
- Task CPU-Bound: Jika sistem memiliki 4 CPU core dan workload sepenuhnya CPU-bound, menambah lebih dari 4 goroutine tidak membantu. Goroutine tambahan hanya akan menambah overhead context-switching.
- Task I/O-Bound: Hardware yang mendasarinya (misalnya disk atau jaringan) mungkin memiliki batasan jumlah operasi simultan yang dapat ditangani.
Contoh
package main
import (
"fmt"
"io"
"net/http"
"sync"
)
func fetchURL(url string, wg *sync.WaitGroup) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
}
func main() {
var wg sync.WaitGroup
url := "https://example.com"
for i := 0; i < 1000; i++ { // Luncurkan 1000 request konkuren
wg.Add(1)
go fetchURL(url, &wg)
}
wg.Wait()
fmt.Println("All requests completed")
}Dalam contoh ini:
- Jika jaringan atau server tidak dapat menangani 1000 koneksi simultan, request akan gagal atau diantrekan, sehingga membatasi performa.
Cara Mengoptimalkan
Jika Anda mencapai titik diminishing returns dengan goroutine, pertimbangkan strategi berikut:
Kontrol Konkurensi Goroutine
Gunakan worker pool untuk membatasi jumlah goroutine yang berjalan secara konkuren. Ini mengurangi contention dan penggunaan sumber daya.
Contoh: Worker Pool
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(time.Second) // Simulasi kerja
results <- job * 2
}
}
func main() {
const numJobs = 5
const numWorkers = 2
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
for w := 1; w <= numWorkers; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= numJobs; a++ {
fmt.Println("Result:", <-results)
}
}Load Balancing
Distribusikan pekerjaan secara lebih merata ke seluruh sumber daya atau interface.
Optimalkan Penggunaan Sumber Daya
Profile aplikasi Anda menggunakan alat seperti pprof untuk mengidentifikasi bottleneck di memori, CPU, atau I/O.
Last updated on