Work Stealing di Go
π 1. Apa itu Work-Stealing?
Work-stealing adalah strategi penjadwalan (scheduling strategy) untuk mendistribusikan beban kerja (task) secara efisien di antara worker thread.
π Intinya:
Setiap worker memiliki queue tugas sendiri (biasanya deque: double-ended queue).
Kalau worker selesai dengan antriannya sendiri dan masih idle, dia akan "mencuri" tugas dari worker lain yang masih sibuk.
Dengan ini, beban kerja merata, thread starvation (worker menganggur) berkurang.
π 2. Bagaimana di Golang?
Di Golang, work-stealing diimplementasikan pada level runtime scheduler, khususnya untuk manajemen Goroutine.
π§ Konsepnya:
Setiap P (
Processor
) punya antrian lokal berisi goroutine.Jika M (
Machine
= OS Thread) idle, dia bisa mencuri goroutine dari antrian P lain.Tujuannya: Menjaga semua core CPU tetap sibuk.
π 3. Ilustrasi Arsitektur:
Goroutine -> dijadwalkan ke P -> dieksekusi oleh M -> idle M bisa mencuri dari P lain.
π 4. Contoh Kode Sederhana
Karena work-stealing terjadi di runtime, kita tidak perlu mengatur secara manual. Tapi kita bisa mensimulasikan perilaku beban kerja tak seimbang supaya runtime memicu work-stealing.
Berikut contoh program Go yang memperlihatkan banyak goroutine dengan beban kerja berbeda:
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
func worker(id int, jobs <-chan int) {
for job := range jobs {
time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond) // simulasi kerja
fmt.Printf("Worker %d menyelesaikan job %d\n", id, job)
}
}
func main() {
runtime.GOMAXPROCS(4) // misalnya kita pakai 4 core
jobs := make(chan int, 100)
// Buat 4 worker goroutine
for w := 1; w <= 4; w++ {
go worker(w, jobs)
}
// Kirim 20 job
for j := 1; j <= 20; j++ {
jobs <- j
}
close(jobs)
// Tunggu agar worker selesai
time.Sleep(5 * time.Second)
}
π§© Penjelasan:
Kita punya 4 worker goroutine.
Masing-masing job durasinya random β ada worker yang cepat selesai lebih dulu.
Worker idle akan mencuri dari antrian global (atau local P lain).
Runtime yang mengatur work-stealing β developer tidak perlu atur manual.
π 5. Inti Penting
β Work-stealing membuat Golang efisien:
Tidak semua goroutine numpuk di satu core.
Beban dibagi otomatis.
Cocok untuk beban kerja yang tidak merata (irregular parallelism).
π Ringkas:
Siapa yang mengatur?
Runtime Scheduler (M, P, G)
Tujuan?
CPU core selalu sibuk
Bagaimana?
Mencuri goroutine dari antrian P lain
Developer?
Hanya fokus bikin goroutine, runtime yang atur
π Gambaran
Ada beberapa worker.
Setiap worker punya queue lokal (deque).
Worker akan memproses task dari queue sendiri.
Kalau queue sendiri kosong, worker akan mencuri task dari queue worker lain.
π Contoh Kode: Work-Stealing Manual
Berikut implementasi sederhana, hanya untuk demo (tidak thread-safe optimal, tapi ilustratif).
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// Task adalah fungsi yang dikerjakan worker
type Task func()
// Deque sederhana (queue double-ended)
type Deque struct {
tasks []Task
lock sync.Mutex
}
func NewDeque() *Deque {
return &Deque{
tasks: []Task{},
}
}
func (d *Deque) PushBottom(task Task) {
d.lock.Lock()
defer d.lock.Unlock()
d.tasks = append(d.tasks, task)
}
func (d *Deque) PopBottom() Task {
d.lock.Lock()
defer d.lock.Unlock()
if len(d.tasks) == 0 {
return nil
}
task := d.tasks[len(d.tasks)-1]
d.tasks = d.tasks[:len(d.tasks)-1]
return task
}
func (d *Deque) StealTop() Task {
d.lock.Lock()
defer d.lock.Unlock()
if len(d.tasks) == 0 {
return nil
}
task := d.tasks[0]
d.tasks = d.tasks[1:]
return task
}
// Worker
type Worker struct {
id int
deque *Deque
pool []*Worker
wg *sync.WaitGroup
}
func (w *Worker) Run() {
for {
task := w.deque.PopBottom()
if task != nil {
task()
w.wg.Done()
continue
}
// Coba mencuri dari worker lain
stolen := false
for _, other := range w.pool {
if other == w {
continue
}
task = other.deque.StealTop()
if task != nil {
fmt.Printf("Worker %d mencuri task dari Worker %d\n", w.id, other.id)
task()
w.wg.Done()
stolen = true
break
}
}
if !stolen {
// Semua queue kosong, istirahat sebentar
time.Sleep(10 * time.Millisecond)
}
}
}
func main() {
numWorkers := 4
workers := make([]*Worker, numWorkers)
wg := &sync.WaitGroup{}
// Buat worker + deque
for i := 0; i < numWorkers; i++ {
workers[i] = &Worker{
id: i,
deque: NewDeque(),
pool: workers,
wg: wg,
}
}
// Jalankan worker
for _, w := range workers {
go w.Run()
}
// Buat 20 task secara acak ke worker
numTasks := 20
for i := 0; i < numTasks; i++ {
wg.Add(1)
taskID := i
task := func() {
fmt.Printf("Task %d dikerjakan oleh goroutine\n", taskID)
time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
}
// Distribusikan ke worker random
worker := workers[rand.Intn(numWorkers)]
worker.deque.PushBottom(task)
}
wg.Wait()
fmt.Println("Semua task selesai!")
}
π Cara Kerja
β Deque:
PushBottom
β Worker menambahkan task ke bawah deque-nya sendiri.PopBottom
β Worker mengambil task dari bawah deque sendiri.StealTop
β Worker lain mencuri task dari atas deque orang lain.
β Worker Loop:
Kerjakan task sendiri.
Kalau habis, coba steal dari deque worker lain.
Kalau semua habis, tunggu sebentar, lalu ulang.
π Output
Kalau dijalankan, kamu akan lihat output mirip:
Task 0 dikerjakan oleh goroutine
Task 1 dikerjakan oleh goroutine
Worker 1 mencuri task dari Worker 0
...
Semua task selesai!
π Catatan Penting
Ini demo β deque pakai
sync.Mutex
supaya aman diakses dari beberapa worker.Versi runtime scheduler Go aslinya jauh lebih rumit & dioptimasi.
Real-world work-stealing pakai deque lock-free seperti Chase-Lev deque.
π π 1. Apa itu Go Runtime Scheduler?
Golang punya green-thread scheduler bawaan. Artinya: goroutine = lightweight thread buatan user β dipetakan ke thread OS nyata. Runtime Scheduler bertugas:
Menjadwalkan jutaan goroutine ke thread OS yang jumlahnya terbatas.
Mengatur distribusi beban kerja (work stealing).
Mengatur preemptive scheduling (bisa pause/resume goroutine).
π π§© 2. Komponen Utama: M, P, G
Go memakai model M-P-G (Machine, Processor, Goroutine).
G (Goroutine)
Unit eksekusi paling kecil β tugas kamu.
M (Machine)
OS thread nyata yang menjalankan G.
P (Processor)
Struktur logis: menyimpan queue G & resource eksekusi (context).
π Rincian
β
G (Goroutine)
Representasi unit kerja.
Punya stack kecil β runtime bisa perbesar/mperkecil.
Ribuan G bisa aktif β cheap dibanding thread OS.
Runtime memetakan G ke M melalui P.
β
M (Machine)
Adalah OS Thread nyata (
pthread
di Linux/Mac).Membawa register, stack, runtime state.
M mengambil G dari P.
Kalau tidak ada P, M tidak bisa kerja β harus punya P.
β
P (Processor)
Ini bukan CPU fisik, tapi slot logis di runtime.
Punya queue lokal: antrian G (siap dijalankan).
Menyediakan resource seperti memory cache, scheduler context.
Jumlah P diatur oleh
GOMAXPROCS
(default = jumlah CPU core).Contoh:
runtime.GOMAXPROCS(4)
β runtime membuat 4 P.
π π 3. Cara Kerja M-P-G Bersama
1οΈβ£ Kamu buat goroutine β misalnya go f()
.
2οΈβ£ Goroutine (G
) masuk ke queue P tertentu.
3οΈβ£ M aktif (thread OS) terhubung ke P.
4οΈβ£ M ambil G dari queue P β jalankan.
5οΈβ£ Kalau P kehabisan G β M bisa:
Curi G dari P lain (work stealing).
Ambil G dari global run queue. 6οΈβ£ Kalau M idle & tidak punya P β parkir, sleep, tunggu P tersedia.
π π§΅ 4. Kenapa Butuh P?
Tanpa P:
M tidak bisa mengeksekusi G.
P = resource execution β kalau P habis, M idle.
Tujuan:
Membatasi jumlah thread aktif.
Mengontrol scaling β 1 M : 1 P.
Membuat concurrency efisien di multi-core.
π π 5. Global Run Queue & Work-Stealing
Jika queue lokal P kosong β P steal dari P lain (work stealing).
Kalau semua P kosong β scheduler mencari G di global run queue.
Work stealing β beban kerja merata β CPU tidak idle.
π β‘ 6. Preemption (Preemptive Scheduling)
Runtime Go mendukung preemptive scheduling:
Goroutine bisa dipaksa stop.
Cegah G rakus CPU terlalu lama.
Membuat concurrency tetap responsif.
π ποΈ 7. Contoh Diagram Sederhana
+-----------+ +-----------+ +-----------+
| M1 | <------> | P1 | <------> | Queue G |
+-----------+ +-----------+ +-----------+
+-----------+ +-----------+ +-----------+
| M2 | <------> | P2 | <------> | Queue G |
+-----------+ +-----------+ +-----------+
M1 & M2 = thread OS nyata.
P1 & P2 = slot logis berisi queue G.
G diproses, queue lokal, work stealing antar P.
π π 8. Kenapa Desain Ini Hebat?
β
Ribuan G β hanya perlu P sesuai core β hemat OS thread.
β
Work-stealing β load balancing otomatis.
β
Preemptive β goroutine lambat tidak block yang lain.
β
Terlihat sederhana buat developer β go f()
β runtime atur semuanya.
π π£ Kesimpulan
G
Unit kerja terkecil
M
Thread OS nyata
P
Slot logis, queue G
GOMAXPROCS
Jumlah P aktif
Work-Stealing
P idle mencuri G dari P lain
Preemption
Runtime pause/resume G
β
1οΈβ£ Kode tracing M
, P
, G
(pakai runtime
package)
M
, P
, G
(pakai runtime
package)Kita tidak bisa melihat M, P, G langsung, tapi bisa lihat dampaknya: berapa banyak goroutine, berapa P
aktif (GOMAXPROCS
), dan ID goroutine.
Berikut contoh:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d start | GOMAXPROCS: %d | NumGoroutine: %d\n",
id,
runtime.GOMAXPROCS(0),
runtime.NumGoroutine(),
)
time.Sleep(time.Second)
}
func main() {
runtime.GOMAXPROCS(2) // Batasi hanya 2 P aktif
wg := &sync.WaitGroup{}
numWorkers := 5
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker(i, wg)
}
fmt.Printf("Jumlah OS thread (NumCPU): %d\n", runtime.NumCPU())
fmt.Printf("Jumlah GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
fmt.Printf("Jumlah goroutine: %d\n", runtime.NumGoroutine())
wg.Wait()
fmt.Println("Selesai semua.")
}
Apa yang terlihat?
GOMAXPROCS(2)
β cuma 2 P.Jalankan 5 goroutine β scheduler akan switch antar goroutine di 2 P.
NumGoroutine
menunjukkan jumlah G.Kalau kamu
runtime.LockOSThread()
, kamu bisa paksa G tetap di 1 M.
β
2οΈβ£ Intip runtime/proc.go
: Struktur M, P, G
runtime/proc.go
: Struktur M, P, GBerikut cuplikan nyata dari sumber kode Go runtime:
// runtime/runtime2.go
type g struct {
stack stack
sched gobuf
m *m // M yang mengeksekusi G ini
// ...
}
type m struct {
g0 *g // Goroutine khusus untuk runtime
curg *g // Goroutine yang sedang berjalan
p *p // Processor yang terhubung
// ...
}
type p struct {
runqhead uint32
runqtail uint32
runq [256]g // queue G lokal P ini
// ...
}
β‘οΈ Makna nyata:
G
menyimpan stack, context, dan pointer ke M.M
punya pointer kecurg
(Goroutine aktif).P
punya queuerunq
β antrian G di P ini.
π Sumber aslinya di: src/runtime/runtime2.go
β
3οΈβ£ Simulasi M, P, G pakai log custom
Kita bikin mini scheduler:
M β OS thread (goroutine di sini).
P β Slot logis dengan queue G.
G β Task.
Tunjukkan worker, queue lokal, dan stealing.
Berikut simulasi M-P-G:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// G = Goroutine (task)
type G struct {
id int
}
func (g *G) run() {
fmt.Printf("Goroutine %d running\n", g.id)
time.Sleep(time.Millisecond * time.Duration(rand.Intn(300)))
}
// P = Processor (queue lokal)
type P struct {
id int
queue []*G
lock sync.Mutex
}
// M = Machine (OS thread)
type M struct {
id int
p *P
wg *sync.WaitGroup
pool []*P
}
func (m *M) run() {
for {
m.p.lock.Lock()
var g *G
if len(m.p.queue) > 0 {
g = m.p.queue[0]
m.p.queue = m.p.queue[1:]
m.p.lock.Unlock()
fmt.Printf("[M %d] eksekusi G %d dari P %d\n", m.id, g.id, m.p.id)
g.run()
m.wg.Done()
} else {
m.p.lock.Unlock()
// Coba stealing dari P lain
stolen := false
for _, otherP := range m.pool {
if otherP == m.p {
continue
}
otherP.lock.Lock()
if len(otherP.queue) > 0 {
g = otherP.queue[0]
otherP.queue = otherP.queue[1:]
otherP.lock.Unlock()
fmt.Printf("[M %d] mencuri G %d dari P %d\n", m.id, g.id, otherP.id)
g.run()
m.wg.Done()
stolen = true
break
}
otherP.lock.Unlock()
}
if !stolen {
time.Sleep(10 * time.Millisecond)
}
}
}
}
func main() {
numP := 2
numM := 2
numG := 10
ps := make([]*P, numP)
for i := 0; i < numP; i++ {
ps[i] = &P{
id: i,
}
}
// Sebarkan G ke P
for i := 0; i < numG; i++ {
p := ps[rand.Intn(numP)]
p.lock.Lock()
p.queue = append(p.queue, &G{id: i})
p.lock.Unlock()
}
wg := &sync.WaitGroup{}
wg.Add(numG)
// Jalankan M, hubungkan ke P
for i := 0; i < numM; i++ {
m := &M{
id: i,
p: ps[i%numP],
wg: wg,
pool: ps,
}
go m.run()
}
wg.Wait()
fmt.Println("Semua G selesai diproses!")
}
π π Penjelasan Simulasi
G
Task yang akan dijalankan
P
Queue lokal, punya lock
M
Goroutine OS, ambil G dari P
Stealing
Kalau queue P kosong, M mencuri dari P lain
β
HASILNYA
Kamu akan melihat M
mencuri
G dari P lain.Mirip cara kerja runtime Go sebenarnya.
Semua M akan aktif selama masih ada G.
π Kesimpulan
β
1οΈβ£ Gunakan runtime
untuk observasi dasar.
β
2οΈβ£ Lihat proc.go
untuk struktur nyatanya.
β
3οΈβ£ Pahami bagaimana work-stealing & P-M-G saling terhubung.
Although both task stealing and continuation stealing have stalling joins, continuations happen
more often than tasks; therefore, the Go scheduling algorithm works with continuations rather
than tasks. The main disadvantage of continuation stealing is that it requires extra work from the
compiler of the programming language. Fortunately, Go provides that extra help and, therefore,
uses continuation stealing in its work-stealing algorithm. One of the benefits of continuation
stealing is that you get the same results when using function calls instead of goroutines or a
single thread with multiple goroutines. This makes perfect sense, as only one thing is executed
at any given point in both cases.
π 1οΈβ£ Konteks Umum
Dalam parallel computing, work stealing artinya worker idle mencuri pekerjaan dari worker lain. Nah, di literatur teori, ada 2 pendekatan:
Task stealing
Continuation stealing
π 2οΈβ£ Apa itu Task vs Continuation?
Task
Unit kerja yang berdiri sendiri. Contoh: fungsi yang harus dijalankan β doWork()
.
Continuation
Bukan tugas baru, tapi kelanjutan eksekusi yang belum selesai.
π Analogi:
Task β βIni 1 dus berisi pekerjaan A. Ambil & kerjakan.β
Continuation β βIni titik berhenti di pekerjaan A. Lanjutkan dari sini.β
π 3οΈβ£ Task stealing β mencuri task
Worker idle akan mencuri task yang masih belum dikerjakan dari worker lain.
Contoh:
Fork-Join β bikin task β simpan ke queue β worker lain bisa ambil.
Contoh di Go: kalau kamu go f()
, maka f
= task.
π 4οΈβ£ Continuation stealing β mencuri kelanjutan eksekusi
Worker idle tidak mencuri task-nya, tapi mencuri kelanjutan eksekusi.
Ini muncul di bahasa functional dan runtime dengan banyak rekursi.
Biasanya: fungsi memanggil fungsi lain β akan ada kelanjutan. Nah, kelanjutannya itulah yang bisa dicuri.
Contoh:
func A() {
B() // B jalan β A βpauseβ dulu
}
Continuation di sini: βapa yang harus A lakukan setelah B selesaiβ.
π 5οΈβ£ Kenapa Go pakai Continuation stealing?
β‘οΈ Goroutine di Go ringan & sangat rekursif β sering bikin kelanjutan dibanding bikin task baru. β‘οΈ Continuation muncul lebih sering β lebih murah & praktis mencuri kelanjutan eksekusi. β‘οΈ Compiler Go membantu split stack, pause/resume β runtime jadi bisa potong di titik tertentu.
π 6οΈβ£ βStalling joinsβ β apa maksudnya?
Kalau punya fork-join, contohnya:
// Fork
go A()
go B()
// Join
Ketika A
& B
selesai β ada titik βjoinβ β harus nunggu β ini bisa bikin stall (idle).
Baik task stealing maupun continuation stealing sama-sama kena join stall.
π 7οΈβ£ Kutipan: βContinuations happen more often than tasksβ
β‘οΈ Artinya: di program nyata, fungsi memanggil fungsi lain β lebih sering terjadi daripada bikin task berdiri sendiri. β‘οΈ Jadi lebih efisien curi continuation daripada bikin task stealing murni.
π 8οΈβ£ βSame results when using function calls instead of goroutinesβ
β‘οΈ Karena scheduler Go bekerja di level continuation:
Panggilan fungsi biasa (
f()
)atau
go f()
β‘οΈ Keduanya bisa dipotong, dijadwal ulang, dicuri β sama saja buat scheduler.
π 9οΈβ£ βDisadvantage: compiler must helpβ
Kenapa compiler harus bantu?
Karena continuation stealing butuh split stack, context switch point, yield.
Compiler Go menambahkan hook agar runtime bisa preempt goroutine β pause/resume.
π 10οΈβ£ Kesimpulan kutipan
Task stealing
Mencuri tugas utuh.
Continuation stealing
Mencuri kelanjutan eksekusi.
Go pakai continuation stealing
Lebih efisien di rekursif & goroutine ringan.
Compiler membantu
Split stack & titik preempt.
Hasil konsisten
Fungsi biasa & goroutine sama-sama dijadwal.
β
Inti Singkat
π Go runtime tidak cuma membagi goroutine β tapi juga memotong & memindahkan kelanjutan eksekusi. Inilah rahasia kenapa Go ringan, preemptive, & bisa work-stealing tanpa developer sadar.
π§© Bagian 1: Entitas utama β M, P, G
βThe Go scheduler works using three main kinds of entities: OS threads (M), which are related to the OS in use, goroutines (G), and logical processors (P).β
β Arti:
Scheduler Go bekerja dengan 3 komponen kunci:
M = Machine β OS Thread nyata β thread yang dibuat & dikelola OS (Linux:
pthread
).G = Goroutine β unit kerja kecil β tugas program yang mau dijalankan.
P = Processor β bukan CPU fisik, tapi slot logis β menyimpan antrian G & resource eksekusi.
π‘ Inti: M = jalan, G = apa yang dijalankan, P = sumber daya + queue untuk memetakan G ke M.
π§© Bagian 2: GOMAXPROCS
βThe number of processors that can be used by a Go program is specified by the value of the GOMAXPROCS environment variableβat any given time, there are, at most, GOMAXPROCS processors.β
β Arti:
GOMAXPROCS
= berapa banyak P aktif di runtime Go.Default = jumlah CPU fisik (
runtime.NumCPU()
).runtime.GOMAXPROCS(4)
artinya: maksimal 4 P aktif β hanya 4 M yang bisa jalan paralel.
π‘ Kenapa dibatasi? Supaya tidak membuat thread OS berlebihan, biar concurrency pas dengan hardware.
π§© Bagian 3: m:n Scheduling
βNow, let us return to the m:n scheduling algorithm used in Go. Strictly speaking, at any time, you have m goroutines that are executed and, therefore, scheduled to run, on n OS threads using, at most, GOMAXPROCS number of logical processors.β
β Arti:
Go pakai m:n scheduling.
m = jumlah Goroutine aktif β bisa jutaan.
n = jumlah thread OS nyata β dibatasi jumlah P.
m:n artinya:
Banyak G (m) di-multiplex ke Thread OS (n).
P = slot eksekusi yang menentukan thread OS mana yang jalan.
π§© Bagian 4: Status Goroutine
βEach goroutine can be in one of the following three stages: executing, runnable, or waiting. In the executing stage, the instructions of the goroutine are executed on an OS thread. In the runnable stage, the goroutine waits to be assigned to an OS thread for execution. Finally, in the waiting stage, the goroutine is blocked for some reason like waiting for a resource or a mutex to become available to go into one of the other two stages.β
β Arti: Setiap Goroutine punya status: 1οΈβ£ Executing β Sedang dijalankan di Thread OS (M) oleh P. 2οΈβ£ Runnable β Siap jalan, belum dapat M + P β antre di queue. 3οΈβ£ Waiting β Tunggu resource, misalnya:
Channel belum ada data.
Mutex dipegang goroutine lain.
I/O block.
β‘οΈ Dari waiting β runnable kalau resource tersedia.
π§© Bagian 5: Antrian Global & Lokal
βThe following figure shows that there are two different kinds of queuesβa global run queue and a local run queueβattached to each logical processor. Goroutines from the global queue are assigned to the queue of a logical processor in order to get executed at some point in the future.β
β Arti:
Global Run Queue:
Semua G yang runnable tapi belum dapat P.
Kalau P lokal kosong, P bisa ambil dari global.
Local Run Queue (di P):
Setiap P punya queue sendiri.
P bekerja dengan queue lokal dulu β lebih cepat.
Kalau queue kosong β curi dari P lain (work stealing) β atau ambil global.
π§© Bagian 6: Stealing
βEach logical processor can have multiple threads, and the stealing occurs between the local queues of the available logical processors.β
β Arti:
Stealing = work stealing.
P1 kosong β dia bisa curi G dari P2.
Ini bikin load balance β tidak ada P yang idle kalau P lain overload.
π§© Bagian 7: Membuat OS Thread Baru
βFinally, keep in mind that the Go scheduler is allowed to create more OS threads when needed. OS threads are expensive in terms of resources and going from one status to another (context switching), which means that dealing too much with OS threads might slow down your Go applications.β
β Arti:
M (thread OS) bisa bertambah kalau:
Ada G blocking syscall.
P butuh M baru buat lanjut.
Tapi thread OS mahal:
Context switch berat.
Stack OS thread lebih besar.
Jadi Go runtime hemat M, makanya scheduler fokus multiplex G ke M.
π§© Bagian 8: Inti GOMAXPROCS
GOMAXPROCS
βNext, we discuss the meaning and the use of GOMAXPROCSβ
β
Artinya:
Selanjutnya, buku ini bakal bahas detail GOMAXPROCS
:
Batas P aktif.
Bagaimana mempengaruhi concurrency.
Bagaimana mempengaruhi M & CPU usage.
π§βπ» Ringkas Visual
+------------------+
| Global Queue | (Goroutine runnable cadangan)
+------------------+
|
v
+-------------------------+ +-------------------------+
| P1 | | P2 |
| Local Run Queue (G) | | Local Run Queue (G) |
+-------------------------+ +-------------------------+
| | | |
v v v v
OS Thread OS Thread OS Thread OS Thread
(M) (M) (M) (M)
π Inti Practical
β
G = apa yang dijalankan
β
M = siapa yang menjalankan (thread OS)
β
P = slot queue & resource
β
GOMAXPROCS
= berapa banyak P aktif
β
Work stealing = kalau P kosong β curi G di P lain
β
Thread OS mahal β Go runtime hemat pakai M secukupnya.
β
π‘ Gambaran Kasar
Eksekusi goroutine di Go runtime β jalur begini:
Program membuat Goroutine (G)
|
Masuk Global Run Queue atau Local Run Queue (P)
|
P memilih M (thread OS)
|
M mengeksekusi G
|
G selesai β M ambil G lain β loop
π 1οΈβ£ Kamu buat Goroutine
Contoh:
go myFunction()
β‘οΈ Go runtime:
Buat
G
baru (structg
diruntime
).Simpan pointer
myFunction
+ stack awal + state βstatus = runnable
.
π 2οΈβ£ Scheduler menaruh G ke Queue
β‘οΈ Ada 2 kemungkinan:
Kalau P (Processor) lokal belum penuh, G masuk ke Local Run Queue milik P.
Kalau P lagi sibuk atau queue lokal penuh β G masuk Global Run Queue.
G -> Local Run Queue (P)
atau
G -> Global Run Queue
Kenapa? Runtime lebih suka queue lokal (lebih cepat diakses β cache friendly). Kalau penuh, sebagian G dilempar ke global biar P lain bisa ambil.
π 3οΈβ£ P yang aktif cari G
P punya queue lokal (
runq
).Kalau ada G di queue β P suruh M jalankan.
Kalau queue lokal kosong:
P cari G di global queue.
Atau work steal dari P lain.
P: Ada kerjaan?
- Ya β eksekusi G.
- Tidak β steal.
π 4οΈβ£ P membutuhkan M untuk eksekusi
P tidak bisa eksekusi sendiri, dia hanya slot logis.
Butuh M (Machine): thread OS nyata.
Jadi:
P βserahkanβ G ke M.
M jalankan G di OS thread.
β‘οΈ 1 P β 1 M β jalankan 1 G (praktis).
π 5οΈβ£ M mulai jalan
M memanggil fungsi G β instruksi jalan.
Stack pointer & context diatur.
Kalau G blocking (misal syscall):
M bisa parkir G β cari G lain.
Kalau M harus block, runtime bisa bikin M baru.
π 6οΈβ£ G selesai
Kalau G selesai β status jadi
dead
β resource dikembalikan.M minta G baru ke P.
Loop lagi ke queue lokal β global queue β work stealing β dst.
π 7οΈβ£ GOMAXPROCS membatasi berapa banyak P aktif
Jumlah P =
GOMAXPROCS
.Kalau kamu
GOMAXPROCS = 4
:Maksimum 4 P aktif.
Artinya hanya 4 M bisa menjalankan G paralel.
Sisanya ngantri β
runnable
.
β
π Alur Ringkas
1. Buat G
go f()
β runtime bikin G
2. Masuk Queue
G β Local P Queue β atau Global Queue
3. P aktif
P ambil G dari queue
4. M butuh P
P butuh M β M jalankan G
5. Eksekusi
M jalankan G di OS Thread
6. Selesai?
G selesai β M ambil G lain
7. P & GOMAXPROCS
Jumlah P dibatasi β thread OS hemat
β
π Diagram Mermaid (Flowchart)
Ini diagram flowchart mermaid β kalau kamu pakai VSCode Extension atau https://mermaid.live
bisa copy-paste ini:
flowchart TD
A[Program membuat Goroutine] --> B{Queue Mana?}
B --> |Local P Queue| C[Masuk Local Run Queue]
B --> |Global Queue| D[Masuk Global Run Queue]
C --> E[P aktif Ambil G]
D --> E
E --> F{P butuh M?}
F --> |Ya| G[P pasang M aktif]
G --> H[M jalankan G di OS Thread]
H --> I{G selesai?}
I --> |Ya| J[G Selesai, Resource Kembali]
I --> |Block?| K[G Blocked, M cari G lain]
J --> E
K --> E
β
π Kesimpulan
G
Unit kerja
P
Slot logis + Queue G
M
Thread OS yang jalanin G
Queue Lokal
Utama
Queue Global
Cadangan, fallback
Work stealing
Kalau P kehabisan, curi dari P lain
GOMAXPROCS
Batasi P aktif β batasi concurrency nyata
Banyak orang paham M, P, G di Go separuh-separuh karena bagian βkenapa P butuh Mβ sering bikin bingung. Yuk, kita bongkar sejelas-jelasnya.
β
1οΈβ£ Kita ulang definisi dulu
G (Goroutine)
Unit kerja.
P (Processor)
Logical Processor: Slot logis di Go runtime.
M (Machine)
Machine: Thread OS nyata (pthread).
π 2οΈβ£ Fungsi P
P = Slot logis. Di dalam P:
Ada queue G (lokal).
Ada context eksekusi (GC, mem alloc, scheduler local data).
Tugas P:
Menyimpan & mengelola antrian G.
Membuat jalur βsiapa next G yang akan dijalankanβ.
TAPI: P tidak bisa mengeksekusi bytecode. P bukan CPU fisik β hanya manajemen.
π 3οΈβ£ Fungsi M
M = Thread OS nyata β dialah yang benar-benar jalan di CPU.
M-lah yang:
Memuat stack G ke register CPU.
Menjalankan instruksi.
Berhubungan langsung dengan kernel (syscall, context switch).
β‘οΈ Tanpa M, P tidak bisa bikin G berjalan di CPU.
π 4οΈβ£ Kenapa P butuh M?
β‘οΈ Eksekusi nyata = CPU β hanya thread OS (M) yang bisa bicara ke kernel + CPU.
β‘οΈ P = logis. Jadi kalau P punya G, dia butuh mesin nyata (M) untuk mengeksekusi G di hardware.
Analogi sederhana:
π§ P = Jalur produksi + tumpukan order π M = Pekerja pabrik π¦ G = Order kerja
P punya tumpukan order (queue G).
Tapi P tidak punya tangan & kaki β dia butuh pekerja nyata (M) untuk memproses barang (G).
π 5οΈβ£ Kenapa M butuh P?
Kebalikannya:
M tidak bisa jalan sendirian.
M butuh P untuk:
Dapat queue G.
Dapat mem alloc data, runtime context.
Tanpa P, M tidak tahu mau kerjakan apa.
β‘οΈ Runtime Go: 1 M aktif = harus punya 1 P aktif.
π 6οΈβ£ Kenapa Go mendesain begitu?
Kenapa tidak langsung G β M?
Karena:
Go mau control concurrency supaya sesuai jumlah core β
GOMAXPROCS
= jumlah P.Kalau M bisa jalan tanpa P β runtime bisa membuat thread OS sebanyak G β boros resource.
P membatasi seberapa banyak eksekusi paralel nyata. Contoh:
GOMAXPROCS = 4 β hanya 4 P β hanya 4 M bisa aktif paralel.
Kalau G banyak, tetap antre di queue P β work stealing.
π 7οΈβ£ Contoh Jalur
G (task) --> P (queue + slot) --> M (thread OS) --> CPU
β P:
Manage queue.
Manage context.
Batasi concurrency.
β M:
Jalankan G nyata di CPU.
Handle syscall.
Context switch ke kernel.
π 8οΈβ£ Visual Sederhana
graph TD
subgraph User Code
A[go myFunc()]
end
A --> B[G]
B --> C[Local Run Queue (P)]
C --> D{M Ada?}
D --> E[M jalankan G]
E --> F[CPU Kerjakan Instruksi]
π 9οΈβ£ Inti
P
βJalur logisβ, queue G, resource control.
M
βEksekutor nyataβ, thread OS β jalankan G di hardware.
G
Task yang diatur & dieksekusi.
P tidak bisa kerja tanpa M β M tidak punya kerja tanpa P.
β
π Kesimpulan
P = logis, M = fisik. P = manajer shift. M = pekerja nyata. P atur tumpukan kerja & jalur. M eksekusi ke CPU.
Apa yang dimaksud βcontext switchingβ di Go runtime.
β
π 1οΈβ£ Potongan yang kamu kutip
"If you decide to assign a value to GOMAXPROCS that is smaller than the number of cores in your machine, you might affect the performance of your program. However, using a GOMAXPROCS value that is larger than the number of available cores does not necessarily make your Go programs run faster due to the context switching of threads."
β‘οΈ Artinya:
Kalau
GOMAXPROCS
kekecilan, kamu tidak memanfaatkan semua core β underutilized.Kalau
GOMAXPROCS
kebanyakan, bukan berarti makin cepat β karena context switching overhead.
β
π 2οΈβ£ Apa maksud βcontext switchingβ?
Context switching = proses CPU berpindah dari 1 thread ke thread lain.
π Levelnya?
Level OS thread (M).
BUKAN di P β P itu hanya logical slot, bukan eksekusi nyata.
CPU hardware hanya tau M β karena M = thread nyata (pthread).
Jadi: β‘οΈ Context switch = CPU nyimpan state M1 β load state M2 β jalankan M2. β‘οΈ Kenapa mahal?
Register CPU harus disimpan & di-load lagi.
Kernel scheduler harus berpindah.
Cache CPU invalid β overhead.
β
π 3οΈβ£ Apa kaitannya dengan GOMAXPROCS
?
GOMAXPROCS
?GOMAXPROCS
= jumlah P aktif.1 P = butuh 1 M aktif untuk mengeksekusi.
Jadi:
GOMAXPROCS
~ jumlah M aktif.
Contoh:
Core CPU = 4 β idealnya
GOMAXPROCS
= 4.Kalau
GOMAXPROCS
= 8:Ada 8 P.
Runtime akan bikin hingga 8 M.
Tapi hanya 4 core CPU β OS harus ganti-ganti thread (M) di core yang sama β context switching meningkat.
β‘οΈ Hasilnya? Lebih banyak switch β lebih banyak overhead β program bisa lebih lambat daripada GOMAXPROCS = 4
.
β
π 4οΈβ£ Flow: Context Switch
Berikut flow sederhana kalau GOMAXPROCS
lebih besar dari core:
sequenceDiagram
participant P1 as P1
participant M1 as M1 (Thread OS)
participant M2 as M2 (Thread OS)
participant CPU as CPU Core
P1->>M1: M1 jalankan G di P1
M1->>CPU: M1 jalan di Core
Note over CPU: Kernel scheduler detect M2 ready
CPU->>M1: Suspend M1 (save context)
CPU->>M2: Load M2 (restore context)
M2->>CPU: M2 jalan di Core
β‘οΈ Perpindahan M1 β M2 = Context Switch β‘οΈ Ini diatur oleh Kernel, bukan runtime Go.
β
π 5οΈβ£ Kenapa Context Switch Mahal?
Save/Restore Register CPU
Waktu eksekusi habis untuk nyimpan & load.
CPU Cache Thrashing
Data thread sebelumnya kebuang.
Overhead Kernel Scheduler
Kernel perlu ngatur prioritas thread.
β‘οΈ Kalau terlalu sering β latency naik, throughput turun.
β
π 6οΈβ£ Bagaimana cara Go mengurangi overhead ini?
1οΈβ£ Work stealing β bikin G tetap di P lokal β biar M tidak perlu switch ke P lain.
2οΈβ£ P membatasi concurrency β GOMAXPROCS
= seberapa banyak M aktif.
3οΈβ£ Goroutine multiplexing β 1 M jalanin banyak G secara bergiliran, bukan bikin M banyak.
β
π 7οΈβ£ Kesimpulan
GOMAXPROCS
< CPU Core
Tidak maksimal, CPU idle.
GOMAXPROCS
= CPU Core
Ideal β M = core.
GOMAXPROCS
> CPU Core
M > core β OS harus sering switch β overhead tinggi.
β‘οΈ Context switch = level OS thread (M), BUKAN P. β‘οΈ P hanya queue + resource β eksekusi nyata di M, switch-nya di situ.
β
π 8οΈβ£ Flow Ringkas
flowchart TD
A[Program] --> B[Goroutine G]
B --> C[P aktif]
C --> D[M jalankan G]
D --> E[CPU Core]
E --> |Core Penuh| F[OS Kernel jadwalkan switch]
F --> G[Context Switch: M pause]
G --> H[M lain jalan di Core sama]
β
π Inti
Context switching = di level M = thread OS
P hanya slot logis β tidak diswitch.
Kebanyakan M aktif > core = overhead context switch.
Last updated