Portal/Notes πŸ“
Golang

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.


Edit on GitHub

Last updated on

Goroutine: Performa & Tradeoffs | Faisal Affan