Goroutines
A Comprehensive Guide to Goroutines [Burmese]
Google က 2009 မှာ Go ကို develop လုပ်ခဲ့တယ်၊ Simplicity, performance နဲ့ concurrency တွေကိုအဓိကထားပြီး design လုပ်ခဲ့တယ်၊ Static typing, garbage collection နဲ့ သူ့ရဲ့ standard library တွေဟာ web server တွေကနေ distributed application တွေရေးဖို့ထိ အဆင်ပြေတဲ့အတွက် လူကြိုက်များတယ်။ Go မှာ စုစုပေါင်း reserved keywords 25 ခုပဲရှိတယ်။
Go concurrency model မှာ Goroutine ကအဓိကဖြစ်တယ် concurrent task တွေအတွက် Goroutine တွေသည် lightweight ဖြစ်တယ်၊ efficient ဖြစ်တယ်။ ဒီကနေ့ modern software တွေမှာ task တွေအများကြီးကို တပြိုင်တည်း handle လုပ်နိုင်ဖို့က အရေးကြီးတဲ့အချက်တွေမှာတစ်ခုအပါဝင်ဖြစ်တယ်၊ Concurrency အတွက် Goroutine တွေက low-level thread တွေကို abstract လုပ်ပြီး complexity အနည်းဆုံးနဲ့ developer-friendly ဖြစ်အောင် လုပ်ပေးထားတယ်။ realtime system တွေ microservice တွေ implement လုပ်ဖို့အတွက် အရမ်းအရေးပါတယ်။
Goroutine ဆိုတာဘာလဲ
Goroutines ဆိုတာ lightweight threads တွေဖြစ်တယ်၊ Go Runtime ကနေ manage လုပ်တယ်၊ OS ကနေမလုပ်ဘူး။ OS thread တွေနဲ့မတူဘူး။ Goroutine တွေက ဘာကြောင့် ပိုပြီး lightweight ဖြစ်လဲ efficient ဖြစ်လဲဆိုတော့ initial stack size 2-8KB ပဲရှိတယ် လိုအပ်သလောက်ထပ်တိုးသွားမယ်၊ OS thread တွေပေါ်မှာတပြိုင်နက်တည်း အလုပ်လုပ်တယ်လို့ပြောလို့ရမယ်၊ OS thread တွေဆိုရင် initial stack size က 1-8MB ရှိတယ်၊ Goroutine တွေက create လုပ်ရတာ destroy လုပ်ရတာ Context switching တွေမှာ ပိုမြန်တယ်၊ အဓိကက OS Thread တွေကို reuse ပြန်လုပ်တာမျိုးလုပ်တယ်၊ ဥပမာ Thread တစ်ခု create လုပ်ပြီးရင် destroy မလုပ်ဘဲ park လုပ်ထားပြီး လိုမှ ပြန်ထုတ်သုံးတာမျိုးလုပ်တယ်။ ဆိုလိုတာက Goroutine ဆိုတာလည်း OS Thread တွေကိုသုံးတာပဲ ဒါပေမယ့် efficient ဖြစ်အောင် သုံးတဲ့ mechanism တစ်ခုလို့ပြောလို့ရတယ်။
Goroutine တစ်ခုကို function ရှေ့မှာ go keyword သုံးပြီး launch လုပ်ပေးရတယ်။ caller function ဖြစ်တဲ့ main function နဲ့ concurrently run မယ် ဆိုလိုတာက main function သည် သူ့ကိုပြီးတဲ့ထိမစောင့်နေဘူး။
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
fmt.Printf(" Hello , %s!\n", name)
}
func main() {
go sayHello("Alice") // Starts a Goroutine
fmt.Println("Main function")
time.Sleep(time.Second) // Wait to see output
}
ဒီနမူနာမှာ time.Sleep ဘာကြောင့်ထည့်လဲဆိုတော့ Goroutine မပြီးသေးဘဲ program exit ဖြစ်မသွားဖို့အတွက်ဖြစ်တယ်။
Under the Hood
Goroutine တွေ အလုပ်လုပ်ပုံကို အတွင်းကျကျနားလည်ထားရင် ပိုကောင်းတယ်။
Go runtime မှာ Goroutine တွေကို manage လုပ်ဖို့ Go Scheduler ပါတယ်။ Go scheduler သည် M:N scheduling model ကိုသုံးတယ်။ Goroutine (M) နဲ့ OS threads (N) ကို map လုပ်တာလို့နားလည်ထားလို့ရတယ်။ total Goroutine အရေအတွက်ကို GOMAXPROCS နဲ့ ထိန်းပေးလို့ရတယ်။ နောက်တစ်ခုသိထားဖို့လိုတာက preemtive scheduling ကိုသုံးတဲ့အကြောင်းပါ၊ preemtive scheduling ဆိုတာက Goroutine တစ်ခုတည်းကနေ thread တစ်ခုကို အပိုင်စီးထားတာမျိုးမဖြစ်အောင် interrupt လုပ်ပြီး တခြား Goroutine တွေကိုပါပေး run တာမျိုးကိုပြောတာပါ။
Communication between Goroutines
Goroutine တွေက channel တွေကိုသုံးပြီး communicate လုပ်တယ် channel တွေမှာ message passing လုပ်နိုင်တဲ့ mechanism တွေ ပါတယ်။ channel တွေနဲ့ဆို thread-safe communication ရမယ်၊ shared memory issue တွေ ရှောင်ပြီးသားဖြစ်သွားမယ်။
<-chan for receive-only
chan<- for send-only
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int) {
ch <- 42 // Send
}
func main() {
ch := make(chan int) // Unbuffered channel
go producer(ch)
value := <-ch // Receive
fmt.Println(value) // Outputs: 42
time.Sleep(time.Second)
}
Buffered vs Unbuffered Channels
ဒီနေရာမှာ buffer နဲ့ unbuffer ဆိုတာ ဘာလဲသိဖို့လိုမယ်၊ I/O operations တွေမှာ buffer ဆိုတာက data ကို process မလုပ်ခင် store လုပ်တယ်၊ unbuffer ဆိုတာကတော့ immediately process လုပ်တယ် store မလုပ်ဘူးလို့ဆိုတယ်။
Buffered Channel
ch := make(chan int,2)
data ကိုသိမ်းဖို့ size သတ်မှတ်ပေးရတယ်၊ receiver မရှိလည်း send လုပ်လို့ရတယ်။ block မလုပ်ဘူး။
buffer capacity ထက်ကျော်ပြီး send လုပ်ရင်တော့ deadlock ဖြစ်နိုင်တယ်။
Unbuffered Channel
ch := make(chan int)
size သတ်မှတ်ပေးစရာမလိုဘူး၊ sender နဲ့ receiver နှစ်မျိုးစလုံး ready ဖြစ်တဲ့အထိ block လုပ်တယ်။ synchronize ဖြစ်တယ်ပေါ့။
Select Statement
channel operation တွေကို တစ်နေရာတည်းမှာတစ်ပြိုင်နက်တည်း handle လုပ်ချင်ရင် select statement ကိုသုံးရတယ်။
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
for range 2 {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
Synchronization and Safety
Concurrency မှာဆိုရင် race condition တွေရှိတဲ့အတွက် အဲ့ဒီကနေပေါ်လာတဲ့ ပြဿနာတွေလည်း ရှိလာမယ်။ အဲ့တာတွေကို နားလည်ထားရင် သတိထားနိုင်မယ်၊ ဖြေရှင်းလို့လွယ်မယ်ပေါ့။
အောက်မှာ Race condition နမူနာကိုကြည့်ပါ
package main
import (
"fmt"
"time"
)
func main(){
counter := 0
for i := 0; i < 1000; i++ {
go func (){
counter++
}()
}
time.Sleep(time.Second)
fmt.Println(counter) // Unpredictable result
}
အဲ့လို shared resource ကိုတွေ concurrent access မဖြစ်အောင်ရယ် Synchronize ဖြစ်အောင်ရယ်ဆိုရင် sync package ရှိမယ် သူ့မှာ Mutex နဲ့ WaitGroup ဆိုတာရှိတယ်။ Mutex ကတော့ Mutual exclusion ပေါ့ resource တွေကို lock လုပ်ဖို့အတွက်သုံးတယ်၊ WaitGroup ကတော့ Synchronize လုပ်ဖို့ တစ်ခုပြီးမှ တစ်ခုလုပ်ဖို့ပေါ့။
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
var wg sync.WaitGroup
counter := 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
mu.Lock()
counter++
mu.Unlock()
wg.Done()
}()
}
wg.Wait()
fmt.Println("Final counter value:", counter)
}
Atomic Operations
simple data type တွေအတွက် lock-free operation တွေကို sync/atomic package သုံးပြီးလုပ်လို့ရတယ်။ mutex တွေထက် simple operation တွေလုပ်တဲ့အခါမှာ ပိုမြန်တယ်၊ scope အကန့်အသတ်တော့ရှိမယ်။
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int32
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
atomic.AddInt32(&counter, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println("Final counter value:", counter)
}
Common Pitfalls
Goroutine တွေမှာ developer တွေ သတိထားရမယ့် challenge တွေရှိတယ်
Goroutine Leaks
Goroutine တွေကို terminate မလုပ်မိတဲ့အခါမျိုးတွေမှာ resource တွေမလိုအပ်ဘဲ consume လုပ်နေတာမျိုးရှိနိုင်ပါတယ်။ အောက်ကနမူနာကိုကြည့်ပါ။
package main
func main() {
ch := make(chan int)
go func() {
<-ch // Blocks forever if no sender
}()
// select {}
// enable above line to see the deadlock situation
}
ဒီလိုမျိုး leak တွေမဖြစ်အောင် exit condition တွေရှိရမယ်၊ မသုံးတော့တဲ့ channel တွေကို close လုပ်ပေးရမယ်၊ cancel လုပ်လို့ရတာတွေကိုလုပ်ပေးရမယ်။
Deadlocks from Unread Channels
Unbuffered channel ကို receiver မပါဘဲ အောက်မှာလို send တစ်ခုပဲ ပါတာမျိုး ဆို deadlock ဖြစ်မယ်၊ Unbuffered channel တွေသည် sender, receiver နှစ်ခုစလုံး ready မဖြစ်မချင်း block လုပ်မယ်ဆိုတာ အထက်မှာပြောခဲ့ပြီးသားဖြစ်ပါတယ်။
package main
func main() {
ch := make(chan int)
ch <- 1 // Deadlock: no receiver
}
Best Practices
Limit Goroutines
Worker pools, Semiphores တွေသုံးပြီးတော့ Goroutine တွေကို limit လုပ်လို့ရပါတယ်။
worker pool example:
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
// process job
results <- j * 2
}
}
func main() {
const numWorkers = 3
jobs := make(chan int, 5)
results := make(chan int, 5)
for w := 1; w <= numWorkers; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= 5; a++ {
fmt.Println(<-results)
}
}
Semiphores example:
sem := make(chan struct{}, 10) // allow max 10 goroutines at once
for _, task := range tasks {
sem <- struct{}{} // acquire
go func(t Task) {
defer func() { <-sem }() // release
process(t)
}(task)
}
Use Context
context ကိုသုံးပြီးတော့ cancellation, timeout တွေ လုပ်လို့ရမယ်။
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ch := make(chan string)
go func() {
select {
case <-ctx.Done():
fmt.Println("Goroutine: context canceled")
case ch <- "Task complete":
fmt.Println("Goroutine: sent task result")
}
}()
time.Sleep(3 * time.Second)
}
Favor Message Passing
Goroutine တွေသုံးပြီး message passing နဲ့ data ကို share လို့ရမယ်။ shared memory သုံးတာကိုတတ်နိုင်သလောက်ရှောင်။
Do not communicate by sharing memory; instead, share memory by communicating.
Close Channels
Data ပို့တာမျိုးမလုပ်တော့ဘူးဆိုရင် Close လုပ်သင့်တဲ့ channel ကိုလုပ်ပါ။ မဟုတ်ရင် resource leak တာတွေ deadlock ပြဿနာတွေကြုံရပါလိမ့်မယ်။
Real-World Use Cases
Goroutines တွေကို web servers တွေမှာ concurrent requests တွေကို handle လုပ်ဖို့သုံးတယ်၊
Background task တွေ လုပ်ဖို့သုံးတယ်၊ Parallel processing လုပ်တဲ့နေရာတွေမှာ အဓိကသုံးတယ်။
သိထားသင့်တဲ့ tools တွေဘာတွေရှိမလဲဆိုရင် Race detector ရှိမယ်၊ Tracing and Profiling လုပ်ဖို့အတွက်သုံးတာတွေ၊ Runtime Diagnostics လုပ်တာတွေသိထားဖို့လိုမယ်။
Race Detector
Data race တွေကို အောက်က CLI နဲ့ detect လုပ်နိုင်တယ်။
go run -race main.go
Tracing
အောက်က CLI ကိုသုံးပြီး Goroutine scheduling နဲ့ bottlenecks တွေကို visualize လုပ်လို့ရတယ်။
go tool trace
Profiling
pprof ကိုသုံးပြီး CPU, memory usage တွေကို monitor လုပ်လို့ရမယ်။
အောက်က program ကို run ထားပြီး http://localhost:6060/debug/pprof မှာ profile တွေကြည့်လို့ရတယ်။
package main
import (
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
// Starts the pprof HTTP server on default port 6060
http.ListenAndServe("localhost:6060", nil)
}()
// Program logic
}
Runtime Diagnostics
ဒီအတွက်ကတော့ runtime.NumGoroutine() သုံးပြီး active Goroutines တွေ၊ leak ဖြစ်နေတာတွေကို monitor လုပ်လို့ရတယ်။
နိဂုံးချုပ်ရရင် Go Concurrency Model မှာ Goroutine တွေကအခြေခံအကျဆုံးဖြစ်တာမလို့ သေချာ နားလည်ထားဖို့လိုပါတယ်။
Last updated