Dasar-Dasar Goroutine
Goroutine
Apabila diminta untuk menceritakan keunggulan golang kepada Gopher, sering muncul tulisan terkait Konkurensi (Concurrency). Dasar dari konten tersebut adalah goroutine yang ringan dan mudah diproses. Berikut adalah ulasan singkat mengenainya.
Konkurensi (Concurrency) vs Paralelisme (Parallelism)
Sebelum memahami goroutine, saya ingin membahas dua konsep yang sering membingungkan.
- Konkurensi: Konkurensi berkaitan dengan pemrosesan banyak pekerjaan dalam satu waktu. Ini bukan berarti pekerjaan tersebut benar-benar dieksekusi secara simultan, tetapi merupakan konsep struktural dan logis di mana beberapa tugas dibagi menjadi unit-unit kecil dan dieksekusi secara bergantian, sehingga pengguna melihat seolah-olah beberapa pekerjaan diproses secara bersamaan. Konkurensi dimungkinkan bahkan pada single core.
- Paralelisme: Paralelisme adalah "pemrosesan beberapa pekerjaan secara simultan pada beberapa core". Sesuai namanya, pekerjaan dijalankan secara paralel, mengeksekusi tugas-tugas yang berbeda secara bersamaan.
Goroutine memungkinkan implementasi konkurensi dengan mudah melalui Go runtime scheduler, dan secara alami memanfaatkan paralelisme melalui pengaturan GOMAXPROCS
.
Multi thread pada Java, yang sering digunakan dengan tingkat pemanfaatan yang tinggi, adalah konsep representatif dari paralelisme.
Mengapa Goroutine Baik?
Ringan (lightweight)
Biaya pembuatannya sangat rendah dibandingkan dengan bahasa lain. Muncul pertanyaan mengapa golang menggunakannya dalam jumlah sedikit? Ini karena lokasi pembuatannya dikelola di dalam Go runtime. Hal ini disebabkan goroutine adalah logical thread ringan, lebih kecil dari unit OS thread, memerlukan ukuran stack awal sekitar 2KB, dan dapat bervariasi secara dinamis dengan penambahan stack sesuai implementasi pengguna.
Dikelola dalam unit stack, pembuatan dan penghapusannya sangat cepat dan murah, memungkinkan pemrosesan yang tidak membebani bahkan ketika menjalankan jutaan goroutine. Berkat Goroutine, runtime scheduler dapat meminimalkan intervensi kernel OS.
Kinerja yang baik (performance)
Pertama, seperti dijelaskan di atas, Goroutine memiliki intervensi kernel OS yang minim, sehingga biaya context switching pada tingkat pengguna (User-Level) lebih murah daripada unit OS thread, memungkinkan pergantian pekerjaan yang cepat.
Selain itu, ia dikelola dengan dialokasikan ke OS thread menggunakan model M:N. Dengan membuat OS thread pool, pemrosesan dapat dilakukan dengan jumlah thread yang sedikit tanpa memerlukan banyak thread. Misalnya, jika terjadi kondisi tunggu seperti system call, Go runtime akan menjalankan goroutine lain pada OS thread tersebut, sehingga OS thread tidak beristirahat dan CPU dimanfaatkan secara efisien, menghasilkan pemrosesan yang cepat.
Oleh karena itu, Golang dapat memberikan kinerja yang lebih tinggi dibandingkan bahasa lain, terutama dalam operasi I/O.
Ringkas (concise)
Keuntungan besar lainnya adalah kemudahan memproses fungsi dengan satu kata kunci go
ketika diperlukan konkurensi.
Penggunaan Lock yang kompleks seperti Mutex
, Semaphore
harus digunakan, dan jika Lock digunakan, kondisi DeadLock yang harus dipertimbangkan menjadi tak terhindarkan, sehingga memerlukan tahapan yang kompleks sejak fase desain sebelum pengembangan.
Goroutine merekomendasikan transfer data melalui Channel
sesuai filosofi "Jangan berkomunikasi dengan berbagi memori; bagilah memori dengan berkomunikasi," dan SELECT
mendukung fungsionalitas yang memungkinkan pemrosesan dimulai dari channel yang datanya sudah siap, dikombinasikan dengan Channel. Selain itu, dengan menggunakan sync.WaitGroup
, kita dapat dengan mudah menunggu hingga semua goroutine selesai, sehingga aliran kerja mudah dikelola. Berkat alat-alat ini, masalah persaingan data antar thread dapat dicegah, dan penanganan konkurensi menjadi lebih aman.
Selain itu, dengan menggunakan context, dimungkinkan untuk mengontrol siklus hidup, pembatalan, timeout, deadline, dan cakupan permintaan pada tingkat pengguna (User-Level), sehingga menjamin tingkat stabilitas tertentu.
Pekerjaan Paralel Goroutine (GOMAXPROCS
)
Meskipun telah disebutkan bahwa konkurensi goroutine itu baik, mungkin muncul pertanyaan apakah ia tidak mendukung paralelisme. Jumlah core CPU saat ini telah melampaui dua digit, berbeda dengan masa lalu, dan PC rumahan juga memiliki jumlah core yang tidak sedikit.
Namun, Goroutine juga menjalankan pekerjaan paralel, yaitu GOMAXPROCS
.
Jika GOMAXPROCS
tidak diatur, pengaturannya berbeda tergantung versinya.
Sebelum 1.5: Nilai default 1, pengaturan wajib jika diperlukan lebih dari 1, seperti
runtime.GOMAXPOCS(runtime.NumCPU())
.1.5 ~ 1.24: Diubah menjadi jumlah semua logical core yang tersedia. Sejak saat ini, pengembang tidak perlu mengaturnya kecuali ada batasan yang sangat diperlukan.
1.25: Sesuai dengan bahasa yang terkenal di lingkungan container, ia memeriksa cGroup pada linux untuk memverifikasi
CPU limit
yang diatur pada container.Maka, jika jumlah logical core adalah 10 dan nilai CPU limit adalah 5,
GOMAXPROCS
akan diatur ke angka yang lebih rendah, yaitu 5.
Perubahan pada versi 1.25 merupakan perubahan yang sangat signifikan. Ini karena utilitas bahasa dalam lingkungan container meningkat. Hal ini mengurangi pembuatan thread yang tidak perlu dan context switching, mencegah CPU throttling.
1package main
2
3import (
4 "fmt"
5 "math/rand"
6 "runtime"
7 "time"
8)
9
10func exe(name int, wg *sync.WaitGroup) {
11 defer wg.Done()
12
13 fmt.Printf("Goroutine %d: mulai\n", name)
14 time.Sleep(10 * time.Millisecond) // Penundaan untuk simulasi pekerjaan
15 fmt.Printf("Goroutine %d: selesai\n", name)
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // Hanya menggunakan 2 CPU core
20 wg := sync.WaitGroup();
21 goroutineCount := 10
22 wg.Add(goroutineCount)
23
24 for i := 0; i < goroutineCount; i++ {
25 go exe(i, &wg)
26 }
27
28 fmt.Println("Menunggu hingga semua goroutine selesai...")
29 wg.Wait()
30 fmt.Println("Semua pekerjaan telah selesai.")
31
32}
33
Scheduler Goroutine (Model M:N)
Jika kita masuk lebih detail pada bagian sebelumnya tentang pengelolaan melalui alokasi ke OS thread menggunakan model M:N, terdapat model goroutine GMP.
- G (Goroutine): Unit pekerjaan terkecil yang dieksekusi di Go
- M (Machine): OS Thread (lokasi pekerjaan yang sebenarnya)
- P (Processor): Proses logis yang dikelola oleh Go runtime
P juga memiliki Local Run Queue tambahan dan berfungsi sebagai scheduler untuk menugaskan G yang dialokasikan ke M. Secara sederhana, goroutine
Proses kerja GMP adalah sebagai berikut:
- Ketika G (Goroutine) dibuat, ia dialokasikan ke Local Run Queue P (Processor).
- P (Processor) menugaskan G (Goroutine) yang ada di Local Run Queue ke M (Machine).
- M (Machine) mengembalikan status G (Goroutine): block, complete, preempted.
- Work-Stealing: Jika Local Run Queue P kosong, P lain akan memeriksa Global Queue. Jika G (Goroutine) juga tidak ada di sana, ia akan "mencuri" pekerjaan dari P (Processor) lokal lain, memastikan semua M terus beroperasi tanpa istirahat.
- Penanganan System Call (Blocking): Jika terjadi Block saat G (Goroutine) berjalan, M (Machine) akan berada dalam kondisi tunggu. Pada saat ini, P (Processor) akan memisahkan diri dari M (Machine) yang ter-Block dan bergabung dengan M (Machine) lain untuk menjalankan G (Goroutine) berikutnya. Dalam hal ini, tidak ada pemborosan CPU bahkan selama waktu tunggu dalam operasi I/O.
- Jika satu G (Goroutine) memonopoli (preempted) terlalu lama, ia akan memberikan kesempatan eksekusi kepada G (Goroutine) lain.
GC (Garbage Collector) Golang juga dijalankan di atas Goroutine, memungkinkan pembersihan memori secara paralel dengan interupsi minimal pada eksekusi aplikasi (STW), sehingga menggunakan sumber daya sistem secara efisien.
Akhir kata, Golang adalah salah satu keunggulan kuat dari bahasa ini, dan masih banyak lagi. Saya berharap banyak pengembang menikmati Go.
Terima kasih.