Tips memahami Goroutines pada Go

Bekerja dengan Goroutines adalah sebuah momok yang membingungkan saya di awal development dengan Go. Goroutines tidak hanya menjadi andalan Go dalam melakukan proses secara simultan, namun juga sebuah skill yang wajib dikuasai dalam perancangan sebuah program menggunakan Go.

Channel

Pada dasarnya goroutines hanyalah sebutan dari sebuah fungsi yang terdapat penggalan kata go di depannya

go func(name string) {
    fmt.Printf("Hello %s", name)
}("Kresna")
Contoh Groroutines pada Go

Sepenggal baris kode diatas merupakan contoh dari goroutines yang melakukan proses cetak ke *stdout* sistem. Kode program tersebut akan berjalan, beriringan dengan proses utama hingga prosesnya selesai. Singkat kata, fungsi tersebut tidak lagi tergantung pada block operation yang terjadi pada proses utamanya.

Kesulitan baru akan muncul ketika main proses atau proses lainnya ingin berinterakasi dengan goroutines tersebut. Karena groroutines memiliki jalurnya sendiri dalam proses simultan, diperlukan sebuah mekanisme yang dapat menjembatani proses goroutines dengan proses lainnya. Mekanisme tersebut bernama Channel.

err := make(chan error)
Contoh Channel dengan tipe data Error

Penggalan kode diatas merupakan contoh dari sebuah channel dengan tipe Error di Go.

Photo by Daniel Jerez / Unsplash

Channel merupakan sebuah interface dari sebuah tipe data atau objek dimana seolah-olah menjadi tunnel atau jembatan yang terhubung antara pengirim dan penerimanya.

func main() {
    result := make(chan int)
    
    go func (res chan<- int, num1, num2 int) {
        time.Sleep(time.Second * 5) // ilustrasi waktu proses
        res<- num1 + num2
        close(res)
    }(result, 10, 20)
    
    fmt.Printf("10 + 20 = %v", <-result)
}
Contoh Program dengan Channel Golang - https://play.golang.org/p/c2dDZ0smjmP

Kutipan kode diatas merupakan ilustrasi channel dengan tipe integer melakukan komunikasi dengan mengirimkan hasil dari penjumlahan ke dalam blok proses utama.

Jika program diatas dijalankan, maka akan proses utama akan menunggu proses dari fungsi goroutines untuk selesai (ilustrasinya membutuhkan waktu 5 detik), setelahnya proses utama akan mencetak hasil ke console atau terminal. Dari sini terlihat bahwa proses utama atau proses yang menerima hasil akan terkena blocking (pause) sampai hasil dari goroutines dikirimkan melalui channel result.

Alur Data pada Channel

...
    res<- num1 + num2
    ...
fmt.Printf("10 + 20 = %v", <-result)
...
Alur pengiriman data channel berdasarkann contoh kode sebelumnya

Alur data flow pada channel ditandai dengan penulisan anak panah ke variabelnya. Mengutip contoh kode sebelumnya, penulisan res<- merupakan statemen untuk mengirimkan data (send) hasil penjumlahan untuk masuk melalui channel, lalu pada main kode program terdapat <-result (receive) yang merupakan proses pengambilan data penjumlahan.

Photo by Jonas Gerg / Unsplash

Buffered Channel

Buffered Channel dapat di analogikan seperti sebuah tunnel yang memiliki kuota mengenai berapa banyak jumlah objek yang dapat muat masuk sekaligus didalamnya.

Secara default sebuah channel tidak memiliki buffer atau bisa disebut juga dengan unbuffered channel, alias hanya satu objek saja yang dapat dikirimkan melalui channel tersebut, sampai objek itu keluar (atau digunakan) dari channel, barulah objek  berikutnya dapat masuk melewati channel yang sama.

Lalu bagaimana jika ingin mengirimkan beberapa objek di satu waktu secara bersamaan? jawabannya dengan menggunakan Buffered Channel.

Dengan penggunaan Buffered Channel kita dapat melakukan proses pengiriman data sekaligus sebanyak jumlah buffer (capacity) yang disiapkan sebelumnya.

package main

import (
    "fmt"
    "time"
)

func add(res chan<- int, num1, num2 int) {
    res <- num1 + num2
    fmt.Println("sent", num1+num2)
}

func printer(t <-chan int) {
    for c := 0; c < 3; c++ {
        time.Sleep(time.Second)
        fmt.Printf("processing %v \n", <-t)
    }
}

func main() {
    results := make(chan int)

    go add(results, 3, 5)
    go add(results, 7, 9)
    go add(results, 2, 12)

    go printer(results)

    time.Sleep(time.Second * 5)
    close(results)
}
Contoh Goroutine dengan Unbuffered Channel - https://play.golang.org/p/JRy7-Qc2Utj

Dalam contoh program diatas, channel results merupakan Unbuffered Channel, yang menjadikan fungsi add() hanya dapat mengirimkan hasil penjumlahan begitu channel results tersedia atau telah melakukan proses receive.

Fungsi printer() sebagai receiver memiliki  waktu proses selama 1 detik, karena melakukan proses menggunakan unbuffered channel, maka antar goroutines add() harus saling menunggu untuk dapat mengirimkan hasil berikutnya.

processing 16 
sent 16
processing 8 
sent 8
processing 14 
sent 14

Program exited.
Hasil ekseskusi dari contoh kode Unbuffered Channel

Terlihat dari hasil eksekusi kode program diatas, fungsi add()  hanya akan dapat mengirimkan data ke channel ketika proses receive telah dilakukan, karena itu proses cetak ke terminal selalu berdampingan antara pengiriman dan pemrosesan data hasil.

Mengambil dari contoh kode sebelumnya, dengan merubah sebaris kode seperti dibawah ini, akan menjadikan proses unbuffered channel menjadi buffered channel dimana channel akan memiliki kuota sebanyak 3x int.

...
func main() {
    results := make(chan int, 3)
...
Buffered Channel dengan nilai 3 untuk tipe int

Berikut kode yang sudah dimodifikasi agar mengadopsi Buffered channel

package main

import (
    "fmt"
    "time"
)

func add(res chan<- int, num1, num2 int) {
    res <- num1 + num2
    fmt.Println("sent", num1+num2)
}

func printer(t <-chan int) {
    for c := 0; c < 3; c++ {
        time.Sleep(time.Second)
        fmt.Printf("processing %v \n", <-t)
    }
}

func main() {
    results := make(chan int, 3)

    go add(results, 3, 5)
    go add(results, 7, 9)
    go add(results, 2, 12)

    go printer(results)

    time.Sleep(time.Second * 5)
    close(results)
}
Full code yang sudah dimodifikasi - https://play.golang.org/p/T80d9Zrjs6G

Dengan menggunakan buffered channel, proses pengiriman hasil dari fungsi add() dapat langsung dilakukan tanpa harus menunggu channel results tersedia. Hal ini terlihat dari hasil output di bawah ini.

sent 16
sent 14
sent 8
processing 16 
processing 14 
processing 8 

Program exited.
Hasil eksekusi dari contoh kode Buffered Channel

Closing Channel

Channel yang telah selesai digunakan harus ditutup dengan keyword close, hal ini menandakan proses transmisi data melalui channel tersebut tidak lagi dapat dilakukan. Selanjutnya channel ini dibersihkan melalui garbage collection.

...
close(results)
...
Close Channel pada Go

GOMAXPROCS

Dalam prakteknya setelah versi go 1.5, default value dari GOMAXPROCS akan diset dengan sejumlah CPU yang tersedia pada komputer yang menjalankannya. Hal ini tentunya merupakan hal yang positif mengingat dalam versi sebelumnya, developer harus mendeteksi berapa jumlah CPU core yang ada dan melakukan set GOMAXPROCS secara manual.

Photo by Dominik Bednarz / Unsplash

Mengapa hal ini sangat berpengaruh? Gorotines pada dasarnya tidak selalu membutuhkan multi CPU untuk bekerja, beberapa developer mungkin berfikir dengan menambah jumlah CPU, jalannya sebuah program akan lebih maksimal, sayangnya tidak selalu seperti itu.

Goroutines mirip seperti Thread pada Java, pada dasarnya menggunakan Memori (RAM) untuk dapat beroperasi.

Goroutines tidak harus selalu berjalan pada CPU yang berbeda untuk memaksimalkan multitasking-nya, karena dalam prosesnya Go secara otomatis akan mengatur berapa dan bagaimana goroutines akan bekerja di satu core ataupun multi-core, karena setelah Go versi 1.5, developer tidak perlu lagi memikirkan bagaimana kolaborasi gorotines dalam memanfaatkan multi-core CPU agar proses dapat bekerja lebih cepat dan efisien.

Berkomunikasilah dengan Data bukan Pointer

Don't communicate by sharing memory; share memory by communicating. (R. Pike)

Kutipan diatas menjelaskan kepada kita tentang bagaimana sebaiknya sebuah proses concurrency berinteraksi antara satu dengan yang lainnya. Meskipun pada Go kita dapat mengirimkan pointer (alamat memori) namun hal itu sebaiknya dihindari ketika menggunakan goroutines.

Melakukan komunikasi antar goroutines dengan cara sharing memory adalah hal yang patut dihindari, karena dapat berpotensi abuse pada alamat memory yang di share ke beberapa proses, dimana memungkinkan munculnya error bila terdapat ketidak konsistenan program dalam memanipulasi memory address. Oleh karena itu dalam goroutines sebaiknya hanya mengirimkan objek statis atau nilai primitif saja.

Semoga tulisan saya ini cukup membantu kalian dalam memahami cara kerja dan aturan-aturan yang ada pada Goroutines.

Kira-kira apa yang menurutmu paling sulit dalam penggunaan goroutines atau channel ini? silahkan tinggalkan komentar dibawah.