1 Pendahuluan tentang Goroutines

1.1 Konsep Dasar Concurrency dan Parallelism

Concurrency dan parallelism adalah dua konsep umum dalam pemrograman multi-threaded. Mereka digunakan untuk mendeskripsikan peristiwa atau eksekusi program yang mungkin terjadi secara bersamaan.

  • Concurrency mengacu pada beberapa tugas yang diproses dalam kerangka waktu yang sama, tetapi hanya satu tugas yang dieksekusi pada waktu tertentu. Tugas dengan cepat bergantian satu sama lain, memberi pengguna ilusi eksekusi serentak. Concurency cocok untuk prosesor single-core.
  • Parallelism mengacu pada beberapa tugas yang benar-benar dieksekusi secara serentak pada waktu yang sama, yang memerlukan dukungan dari prosesor multi-core.

Bahasa Go dirancang dengan concurrency sebagai salah satu tujuan utamanya. Bahasa ini mencapai model pemrograman konkuren yang efisien melalui Goroutines dan Channels. Runtime Go mengelola Goroutines, dan dapat menjadwalkan Goroutines ini pada beberapa thread sistem untuk mencapai pemrosesan paralel.

1.2 Goroutines dalam Bahasa Go

Goroutines adalah konsep inti untuk mencapai pemrograman konkuren di bahasa Go. Mereka adalah thread ringan yang dikelola oleh runtime Go. Dari perspektif pengguna, mereka mirip dengan thread, tetapi menggunakan lebih sedikit sumber daya dan memulai lebih cepat.

Karakteristik Goroutines meliputi:

  • Ringan: Goroutines menggunakan ruang memori stack yang lebih sedikit dibandingkan dengan thread tradisional, dan ukuran stack mereka dapat secara dinamis memperluas atau menyusut sesuai kebutuhan.
  • Overhead rendah: Overhead untuk membuat dan menghancurkan Goroutines jauh lebih rendah daripada thread tradisional.
  • Mekanisme komunikasi sederhana: Channels menyediakan mekanisme komunikasi yang sederhana dan efektif antara Goroutines.
  • Desain non-blocking: Goroutines tidak menghalangi Goroutines lain untuk berjalan dalam operasi tertentu. Misalnya, sementara satu Goroutine menunggu operasi I/O, Goroutines lain dapat terus menjalankan.

2 Membuat dan Mengelola Goroutines

2.1 Cara Membuat Goroutine

Dalam bahasa Go, Anda dapat dengan mudah membuat Goroutine dengan menggunakan kata kunci go. Ketika Anda menambahkan awalan panggilan fungsi dengan kata kunci go, fungsi akan dieksekusi secara asynchronous dalam Goroutine baru.

Mari kita lihat contoh sederhana:

package main

import (
	"fmt"
	"time"
)

// Tentukan fungsi untuk mencetak Hello
func sayHello() {
	fmt.Println("Hello")
}

func main() {
	// Memulai Goroutine baru menggunakan kata kunci go
	go sayHello()

	// Goroutine utama menunggu periode untuk memungkinkan sayHello dieksekusi
	time.Sleep(1 * time.Second)
	fmt.Println("Fungsi utama")
}

Dalam kode di atas, fungsi sayHello() akan dieksekusi secara asynchronous dalam Goroutine baru. Ini berarti fungsi main() tidak akan menunggu sayHello() selesai sebelum melanjutkan. Oleh karena itu, kami menggunakan time.Sleep untuk menghentikan Goroutine utama, memungkinkan pernyataan cetak dalam sayHello dieksekusi. Ini hanya untuk tujuan demonstrasi. Dalam pengembangan sesungguhnya, kita biasanya menggunakan channel atau metode sinkronisasi lainnya untuk mengkoordinasikan eksekusi Goroutine yang berbeda.

Catatan: Dalam aplikasi praktis, time.Sleep() seharusnya tidak digunakan untuk menunggu Goroutine selesai, karena itu bukan mekanisme sinkronisasi yang dapat diandalkan.

2.2 Mekanisme Penjadwalan Goroutine

Di Go, penjadwalan Goroutines ditangani oleh penjadwal Go runtime, yang bertanggung jawab untuk mengalokasikan waktu eksekusi pada prosesor logis yang tersedia. Penjadwal Go menggunakan teknologi penjadwalan M:N (beberapa Goroutines dipetakan ke beberapa thread OS) untuk mencapai kinerja yang lebih baik pada prosesor multi-core.

GOMAXPROCS dan Prosesor Logis

GOMAXPROCS adalah variabel lingkungan yang mendefinisikan jumlah maksimum CPU yang tersedia untuk penjadwal runtime, dengan nilai default adalah jumlah inti CPU pada mesin. Runtime Go menugaskan satu thread OS untuk setiap prosesor logis. Dengan mengatur GOMAXPROCS, kita dapat membatasi jumlah inti yang digunakan oleh runtime.

import "runtime"

func init() {
    runtime.GOMAXPROCS(2)
}

Kode di atas menetapkan maksimum dua inti untuk menjadwalkan Goroutines, bahkan ketika menjalankan program pada mesin dengan lebih banyak inti.

Operasi Scheduler

Scheduler beroperasi menggunakan tiga entitas penting: M (mesin), P (processor), dan G (Goroutine). M mewakili mesin atau thread, P mewakili konteks penjadwalan, dan G mewakili Goroutine tertentu.

  1. M: Mewakili mesin atau thread, berperan sebagai abstraksi dari thread kernel OS.
  2. P: Mewakili sumber daya yang diperlukan untuk menjalankan Goroutine. Setiap P memiliki antrian Goroutine lokal.
  3. G: Mewakili Goroutine, termasuk tumpukan eksekusi, set instruksi, dan informasi lainnya.

Prinsip kerja penjadwal Go adalah:

  • M harus memiliki P untuk menjalankan G. Jika tidak ada P, M akan dikembalikan ke cache thread.
  • Ketika G tidak diblokir oleh G lain (misalnya, dalam pemanggilan sistem), G dijalankan pada M yang sama sebanyak mungkin, membantu menjaga data lokal G 'panas' untuk memanfaatkan cache CPU lebih efisien.
  • Ketika G diblokir, M dan P akan terpisah, dan P akan mencari M baru atau membangunkan M baru untuk melayani G lain.
go func() {
    fmt.Println("Halo dari Goroutine")
}()

Kode di atas menunjukkan memulai Goroutine baru, yang akan mendorong penjadwal untuk menambahkan G baru ke antrian untuk dieksekusi.

Penjadwalan Mendahului Goroutine

Pada tahap awal, Go menggunakan penjadwalan kooperatif, artinya Goroutine dapat membuat Goroutine lain kelaparan jika mereka dieksekusi untuk waktu yang lama tanpa sukarela melepaskan kontrol. Sekarang, penjadwal Go menerapkan penjadwalan mendahului, memungkinkan G yang berjalan lama untuk dijeda untuk memberi kesempatan kepada G lain untuk dieksekusi.

2.3 Manajemen Siklus Hidup Goroutine

Untuk memastikan kekokohan dan kinerja aplikasi Go Anda, memahami dan mengelola dengan benar siklus hidup Goroutine sangat penting. Memulai Goroutine sederhana, tetapi tanpa manajemen yang tepat, dapat menyebabkan masalah seperti kebocoran memori dan kondisi perlombaan.

Memulai Goroutine dengan Aman

Sebelum memulai Goroutine, pastikan untuk memahami beban kerjanya dan karakteristik waktu jalannya. Sebuah Goroutine harus memiliki awal dan akhir yang jelas untuk menghindari menciptakan "yatim Goroutine" tanpa kondisi terminasi.

func pekerja(selesai chan bool) {
    fmt.Println("Sedang Bekerja...")
    time.Sleep(time.Second) // mensimulasikan tugas yang mahal
    fmt.Println("Selesai Bekerja.")
    selesai <- true
}

func utama() {
    // Di sini, mekanisme saluran (channel) digunakan dalam Go. Anda dapat dengan mudah menganggap saluran sebagai antrian pesan dasar, dan menggunakan operator "<-" untuk membaca dan menulis data antrian.
    selesai := make(chan bool, 1)
    go pekerja(selesai)
    
    // Tunggu Goroutine selesai
    <-selesai
}

Kode di atas menunjukkan salah satu cara untuk menunggu Goroutine selesai menggunakan saluran selesai.

Catatan: Contoh ini menggunakan mekanisme saluran dalam Go, yang akan dijelaskan dalam bab-bab selanjutnya.

Menghentikan Goroutine

Secara umum, akhir program akan secara implisit mengakhiri semua Goroutine. Namun, dalam layanan yang berjalan lama, kita mungkin perlu menghentikan Goroutine secara aktif.

  1. Gunakan saluran untuk mengirim sinyal berhenti: Goroutine dapat memeriksa saluran untuk mencari sinyal berhenti.
berhenti := make(chan struct{})

go func() {
    for {
        select {
        case <-berhenti:
            fmt.Println("Mendapat sinyal berhenti. Mematikan...")
            return
        default:
            // lakukan operasi normal
        }
    }
}()

// Mengirim sinyal berhenti
berhenti <- struct{}{}
  1. Gunakan paket context untuk mengelola siklus hidup:
konteks, batalkan := context.WithCancel(context.Background())

go func(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Mendapat sinyal berhenti. Mematikan...")
            return
        default:
            // lakukan operasi normal
        }
    }
}(konteks)

// saat Anda ingin menghentikan Goroutine
batalkan()

Menggunakan paket context memungkinkan kontrol yang lebih fleksibel terhadap siklus hidup Goroutine, memberikan kemampuan timeout dan pembatalan. Dalam aplikasi besar atau mikro layanan, context adalah cara yang direkomendasikan untuk mengendalikan siklus hidup Goroutine.