Cara Kerja Goroutine pada Go: Stack Dinamis 2KB dan Penjadwal GMP M:N

Goroutine
Ketika Gopher diminta menceritakan keunggulan golang, sering muncul pembahasan tentang konkurensi (Concurrency). Dasar dari pembahasan tersebut adalah goroutine, yang ringan dan sederhana untuk diproses. Berikut ini adalah penjelasan singkat mengenai hal tersebut.
Konkurensi (Concurrency) vs Paralelisme (Parallelism)
Sebelum memahami goroutine, izinkan saya terlebih dahulu membahas dua konsep yang sering dikacaukan.
- Konkurensi: Konkurensi berkaitan dengan penanganan banyak pekerjaan sekaligus. Ini tidak berarti dieksekusi secara benar-benar bersamaan, melainkan konsep struktural dan logis yang membagi beberapa tugas menjadi unit-unit kecil dan mengeksekusinya secara bergantian, sehingga bagi pengguna tampak seolah-olah beberapa tugas diproses secara bersamaan. Konkurensi juga dimungkinkan pada satu core tunggal.
- Paralelisme: Paralelisme adalah "memproses beberapa pekerjaan secara bersamaan pada beberapa core". Secara harfiah, pekerjaan dijalankan secara paralel, dan tugas-tugas lain dieksekusi pada waktu yang sama.
Goroutine memudahkan implementasi konkurensi melalui scheduler runtime Go, dan dengan pengaturan GOMAXPROCS, paralelisme pun dimanfaatkan secara alami.
Multi-thread Java, yang penggunaannya sangat luas, merupakan representasi konsep paralelisme.
Mengapa Goroutine Itu Baik?
Ringan (lightweight)
Biaya pembuatannya jauh lebih rendah dibandingkan bahasa lain. Di sini muncul pertanyaan mengapa golang menggunakannya lebih sedikit? Hal ini karena lokasi pembuatannya dikelola di dalam runtime Go. Alasannya, goroutine adalah thread logis yang ringan. Ukurannya lebih kecil daripada unit thread OS, stack awalnya membutuhkan sekitar 2KB, dan stack dapat ditambahkan sesuai implementasi pengguna sehingga ukurannya berubah secara dinamis.
Karena dikelola per stack, pembuatan dan penghapusan sangat cepat dan murah, sehingga menjalankan jutaan goroutine pun tidak menjadi beban. Akibatnya, dengan penjadwal runtime, Goroutine dapat meminimalkan keterlibatan kernel OS.
Performa yang baik (performance)
Pertama, seperti dijelaskan di atas, Goroutine melibatkan kernel OS dalam jumlah kecil sehingga, pada tingkat pengguna (User-Level), biaya context switching lebih murah dibandingkan unit thread OS, sehingga pergantian tugas dapat dilakukan dengan cepat.
Selain itu, model M:N digunakan untuk mengalokasikan dan mengelola goroutine pada thread OS. Dengan membuat thread pool OS, pemrosesan dapat dilakukan dengan jumlah thread yang sedikit tanpa memerlukan banyak thread. Misalnya, ketika terjadi keadaan menunggu seperti system call, runtime Go dapat menjalankan goroutine lain pada thread OS tersebut, sehingga thread OS tidak berhenti dan CPU dimanfaatkan secara efisien untuk pemrosesan yang cepat.
Dengan demikian, Golang dapat menghasilkan performa yang tinggi dibandingkan bahasa lain, khususnya pada pekerjaan I/O.
Ringkas (concise)
Keunggulan besar lainnya adalah kemudahan menjalankan fungsi secara konkuren hanya dengan satu kata kunci go.
Dengan Mutex, Semaphore, dan Lock kompleks lainnya, kita harus memperhatikan kondisi deadlock (DeadLock) yang wajib dipertimbangkan saat menggunakan Lock, sehingga tahap desain sebelum pengembangan pun menjadi rumit.
Goroutine menganut filosofi "jangan berbagi memori dengan berkomunikasi, tetapi berbagi memori dengan berkomunikasi", sehingga pertukaran data melalui Channel sangat dianjurkan. SELECT juga mendukung fungsi yang dikombinasikan dengan Channel untuk memproses channel yang datanya sudah siap terlebih dahulu. Selain itu, dengan sync.WaitGroup, kita dapat menunggu hingga semua goroutine selesai dengan mudah sehingga alur kerja dapat dikelola dengan sederhana. Berkat alat-alat tersebut, masalah data race antar thread dapat dicegah dan pemrosesan konkurensi dapat dilakukan dengan lebih aman.
Selain itu, dengan menggunakan context, kita dapat mengendalikan siklus hidup, pembatalan, timeout, deadline, dan lingkup permintaan pada tingkat pengguna (User-Level), sehingga stabilitas tertentu dapat dijamin.
Pemrosesan Paralel Goroutine (GOMAXPROCS)
Setelah membahas keunggulan konkurensi goroutine, mungkin muncul pertanyaan apakah paralelisme tidak didukung. Jumlah core CPU saat ini sudah melampaui dua digit, dan PC rumahan pun memiliki banyak core.
Namun, Goroutine juga melakukan pemrosesan paralel, dan hal tersebut dilakukan oleh GOMAXPROCS.
Jika GOMAXPROCS tidak diatur, nilainya berbeda-beda tergantung versi.
Sebelum 1.5: Nilai default 1, dan jika ingin lebih dari 1 perlu diatur dengan cara seperti
runtime.GOMAXPOCS(runtime.NumCPU())1.5 ~ 1.24: Berubah menjadi jumlah seluruh core logis yang tersedia. Sejak saat itu, pengembang tidak perlu mengaturnya kecuali ada batasan khusus.
1.25: Sebagai bahasa yang populer di lingkungan container, sistem akan memeriksa cGroup pada linux untuk mengetahui
batas CPUyang diatur pada container.Dengan demikian, jika jumlah core logis adalah 10 dan batas CPU adalah 5, maka
GOMAXPROCSdiatur ke 5, yaitu nilai yang lebih kecil.
Perubahan pada 1.25 memiliki dampak yang sangat besar, karena penggunaan bahasa di lingkungan container menjadi lebih luas. Dengan demikian, pembuatan thread yang tidak perlu dan context switching dapat dikurangi sehingga throttling CPU dapat dicegah.
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: ์์\n", name)
14 time.Sleep(10 * time.Millisecond) // ์์
์๋ฎฌ๋ ์ด์
์ ์ํ ์ง์ฐ
15 fmt.Printf("Goroutine %d: ์์\n", name)
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // CPU ์ฝ์ด 2๊ฐ๋ง ์ฌ์ฉ
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("๋ชจ๋ goroutine์ด ๋๋ ๋๊น์ง ๋๊ธฐํฉ๋๋ค...")
29 wg.Wait()
30 fmt.Println("๋ชจ๋ ์์
์ด ์๋ฃ๋์์ต๋๋ค.")
31
32}
33
Penjadwal Goroutine (Model M:N)
Pada bagian sebelumnya tentang model M:N yang dialokasikan ke thread OS, jika dijelaskan lebih rinci, terdapat model GMP pada goroutine.
- G (Goroutine): Unit pekerjaan terkecil yang dijalankan di Go
- M (Machine): Thread OS (lokasi pekerjaan sebenarnya)
- P (Processor): Proses logis yang dikelola oleh runtime Go
Selain itu, P memiliki antrean eksekusi lokal (Local Run Queue) dan berperan sebagai penjadwal yang menugaskan G yang dialokasikan ke M. Secara sederhana, goroutine bekerja dengan alur GMP sebagai berikut.
- Ketika G (Goroutine) dibuat, ia dialokasikan ke antrean eksekusi lokal P (Processor).
- P (Processor) menugaskan G (Goroutine) yang ada di antrean eksekusi lokal ke M (Machine).
- M (Machine) mengembalikan status G (Goroutine), yaitu block, complete, atau preempted.
- Work-Stealing (mencuri pekerjaan): Jika antrean eksekusi lokal P menjadi kosong, P lain akan memeriksa global queue. Jika tidak ada G (Goroutine) di sana, P tersebut mencuri pekerjaan dari P lokal lain agar semua M terus bekerja tanpa henti.
- Penanganan system call (Blocking): Ketika block terjadi saat G (Goroutine) sedang berjalan, M (Machine) masuk ke keadaan menunggu. Pada saat itu, P (Processor) dipisahkan dari M (Machine) yang terblokir, lalu digabungkan dengan M (Machine) lain untuk menjalankan G (Goroutine) berikutnya. Dengan cara ini, tidak ada pemborosan CPU selama menunggu pekerjaan I/O.
- Jika satu G (Goroutine) menguasai (preempted) terlalu lama, maka kesempatan eksekusi diberikan kepada G (Goroutine) lain.
Golang juga menjalankan GC (Garbage Collector) di atas Goroutine, sehingga memori dapat dibersihkan secara paralel dengan menghentikan eksekusi aplikasi seminimal mungkin (STW), dan sumber daya sistem digunakan secara efisien.
Terakhir, Golang merupakan salah satu keunggulan kuat bahasa ini, dan masih banyak keunggulan lainnya. Semoga banyak pengembang dapat menikmati pemrograman dengan Golang.
Terima kasih.