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