Portal/Notes πŸ“
Golang

Goroutines dan Channels di Go

Memahami goroutine dan channel di Go β€” cara kerja konkurensi, komunikasi antar goroutine, serta buffered vs unbuffered channel.

Versi Bahasa Indonesia. Baca dalam Bahasa Inggris

Goroutines

Goroutine adalah thread ringan (lightweight thread) yang dikelola oleh runtime Go. Ini memungkinkan pemrograman konkuren dengan menjalankan banyak fungsi secara independen dan simultan.

  • Goroutine jauh lebih ringan daripada thread tradisional.
  • Mereka berbagi ruang alamat yang sama, membuatnya efisien.
  • Goroutine dijalankan menggunakan keyword go diikuti dengan pemanggilan fungsi.

Ringan

Goroutine di Go ringan karena dirancang lebih efisien daripada thread OS tradisional. Efisiensi ini berasal dari beberapa optimasi di tingkat runtime yang memungkinkan jutaan goroutine berjalan secara konkuren dengan overhead minimal.

  • Mulai dengan stack kecil (sekitar 2 KB)
    • Thread OS biasanya mulai dengan 2 MB.
    • Stack yang lebih kecil berarti lebih sedikit memori yang dialokasikan di awal.
  • Dikelola oleh scheduler runtime Go di user space, bukan oleh sistem operasi.
    • Go menggunakan model M:N scheduling, di mana M goroutine dimultipleks ke N thread OS.
    • Scheduler memutuskan kapan harus menjeda dan melanjutkan goroutine.
  • Context switching lebih cepat.
    • Karena goroutine berada di level user, hanya beberapa register dan stack pointer yang perlu disimpan.
    • Thread OS memerlukan context switching CPU penuh yang membuatnya lebih lambat.
  • Komunikasi efisien menggunakan channel.

Karena Go menggunakan garbage collector, ada jeda singkat untuk GC melakukan full scanning. Untuk beberapa kasus, jeda singkat ini dapat berdampak besar jika kita menjalankan aplikasi latensi rendah dengan traffic tinggi.

Contoh

package main

import (
    "fmt"
    "time"
)

func printMessage(msg string) {
    for i := 0; i < 5; i++ {
        fmt.Println(msg)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go printMessage("Goroutine 1") // Berjalan di goroutine terpisah
    go printMessage("Goroutine 2") // Goroutine lainnya
    printMessage("Main Function")  // Berjalan di thread utama
}
  • Fungsi printMessage dijalankan secara konkuren oleh beberapa goroutine.
  • Fungsi main itu sendiri berjalan sebagai goroutine.

Channels

Channel adalah mekanisme yang digunakan goroutine untuk berkomunikasi satu sama lain dan menyinkronkan eksekusi mereka. Channel memungkinkan pertukaran data yang aman antar goroutine tanpa perlu locking eksplisit.

  • Channel memiliki tipe; Anda menentukan tipe data yang ditransmisikan.
  • Gunakan fungsi make untuk membuat channel.
  • Gunakan <- untuk mengirim (channel <- value) dan menerima (value := <-channel) data.
  • Channel bisa unbuffered (synchronous) atau buffered (asynchronous).

Contoh

package main

import (
    "fmt"
    "time"
)

func sendMessage(ch chan string) {
    time.Sleep(5 * time.Second)
    ch <- "Hello from Goroutine" // Kirim data ke channel
}

func main() {
    ch := make(chan string) // Buat channel bertipe string

    go sendMessage(ch)          // Mulai goroutine
    message := <-ch             // Terima data dari channel
    fmt.Println(message)        // Output: Hello from Goroutine
}
  • Goroutine sendMessage mengirim data ke channel.
  • Fungsi main menunggu untuk menerima data sebelum melanjutkan.

Interaksi Antara Goroutines dan Channels

  • Mengirim Data: Satu goroutine dapat mengirim data ke channel.
  • Menerima Data: Goroutine lain dapat memblokir hingga menerima data.
  • Sinkronisasi: Unbuffered channel bertindak sebagai titik sinkronisasi, memastikan satu goroutine menunggu yang lain.

Contoh

package main

import "fmt"

// Worker function untuk menghitung jumlah
func calculateSum(numbers []int, result chan int) {
    sum := 0
    for _, num := range numbers {
        sum += num
    }
    result <- sum // Kirim hasil ke channel
}

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // Buat channel untuk mengkomunikasikan hasil
    result := make(chan int)

    // Bagi pekerjaan ke dua goroutine
    go calculateSum(numbers[:len(numbers)/2], result) // Setengah pertama
    go calculateSum(numbers[len(numbers)/2:], result) // Setengah kedua

    // Terima hasil dari kedua goroutine
    sum1 := <-result
    sum2 := <-result

    fmt.Println("Total Sum:", sum1+sum2) // Gabungkan hasil
}
  • Dua goroutine menghitung jumlah parsial secara konkuren.
  • Hasil dikirim kembali melalui channel result.
  • Fungsi main menunggu untuk menerima kedua results sebelum menggabungkannya.

Buffered vs. Unbuffered Channels

Unbuffered Channels

  • Tipe default.
  • Pengirim memblokir sampai penerima siap.
  • Penerima memblokir sampai pengirim mengirim data.
  • Menjamin sinkronisasi.
ch := make(chan int) // Unbuffered channel

Buffered Channels

  • Mengizinkan beberapa nilai untuk diantrekan dalam channel.
  • Pengirim hanya memblokir ketika buffer penuh.
  • Penerima hanya memblokir ketika buffer kosong.
ch := make(chan int, 5) // Buffered channel dengan kapasitas 5

Channel untuk Mencegah Race Condition

Mari kita buat contoh sederhana dari data race di Go. Di sini kita ingin menaikkan variabel counter, tetapi variabel ini akan diakses oleh banyak goroutine.

var counter int

func increment() {
    for range 1_000 {
        counter++ // Race condition di sini
    }
}

func main() {
    for range 10 {
        go increment() // 10 goroutine menaikkan secara konkuren
    }

    time.Sleep(1 * time.Second)
    fmt.Println("Final Counter:", counter)
}
➜ go run main.go
Final Counter: 6897
➜ go run main.go
Final Counter: 4626

Seperti yang kita lihat di atas, program ini mengembalikan nilai counter yang salah. Ini karena setiap goroutine memodifikasi counter tanpa sinkronisasi, menyebabkan data race dan nilai yang tidak akurat.

Ini dapat diperbaiki dengan menggunakan channel.


func increment(ch chan int) {
    for range 1_000 {
        ch <- 1 // Kirim permintaan increment
    }
}

func main() {
    ch := make(chan int)
    counter := 0

    go func() {
        for value := range ch {
            counter += value // Perbarui counter dengan aman
        }
    }()

    for range 10 {
        go increment(ch)
    }

    time.Sleep(1 * time.Second)
    close(ch)
    fmt.Println("Final Counter:", counter)
}
➜ go run main.go
Final Counter: 10000
➜ go run main.go
Final Counter: 10000

Dengan menambahkan channel, kita memastikan bahwa hanya satu goroutine (receiver) yang memperbarui counter. Ini menjamin akses aman ke variabel counter dan mengembalikan nilai yang konsisten dan benar. Metode ini sejalan dengan slogan Go di bawah ini.

Jangan berkomunikasi dengan berbagi memori; sebaliknya, bagikan memori dengan berkomunikasi.

Harap diperhatikan bahwa contoh di atas sangat disederhanakan. Dalam skenario dunia nyata, Anda harus menggunakan sync.WaitGroup untuk menunggu semua goroutine selesai, bukan menggunakan time.Sleep.

Ringkasan

  • Goroutine menyediakan konkurensi, sementara channel memfasilitasi komunikasi dan sinkronisasi.
  • Channel mencegah race condition dengan memungkinkan data mengalir dengan aman antar goroutine.
  • Gunakan unbuffered channel untuk sinkronisasi dan buffered channel untuk komunikasi yang lebih terdecouple.

Referensi


Edit on GitHub

Last updated on

Goroutines dan Channels di Go | Faisal Affan