goroutine
https://gyazo.com/feb956e37ba730ac5122382f946ad7d5
概要
goroutineはGoの軽量なスレッド(グリーンスレッド)です。OSスレッドよりもはるかに軽量で、数百万のgoroutineを同時に実行することができます。 技術的詳細
アーキテクチャ
M:N スレッドモデル
M個のOSスレッドにN個のgoroutineをマッピング
Goのランタイムスケジューラが効率的に管理
Work-stealing algorithmによる負荷分散
メモリフットプリント
code: (go)
// 初期スタックサイズ:2KB(動的に拡張)
// OSスレッド:通常8MB(固定)
スケジューラ(GMP モデル)
G(Goroutine)
実行単位
スタック、プログラムカウンタ、レジスタ状態を保持
M(Machine)
OSスレッド
実際のCPUで実行される
P(Processor)
論理プロセッサ
Goroutineの実行コンテキスト
デフォルトでCPUコア数と同じ
code: (go)
// P の数を設定
runtime.GOMAXPROCS(4)
基本的な使用法
goroutineの起動
code: (go)
package main
import (
"fmt"
"time"
)
func hello(name string) {
fmt.Printf("Hello, %s!\n", name)
}
func main() {
// 通常の関数呼び出し
hello("World")
// goroutineで実行
go hello("Go")
// メイン関数終了まで待機
time.Sleep(time.Second)
}
無名関数でのgoroutine
code: (go)
func main() {
go func() {
fmt.Println("Anonymous goroutine")
}()
// クロージャーの使用
message := "Closure"
go func(msg string) {
fmt.Println(msg)
}(message)
time.Sleep(time.Second)
}
channel による通信
基本的なchannel
code: (go)
func main() {
ch := make(chan string)
go func() {
ch <- "Hello from goroutine"
}()
// channelから受信(ブロッキング)
message := <-ch
fmt.Println(message)
}
バッファ付きchannel
code: (go)
func main() {
// バッファサイズ3のchannel
ch := make(chan int, 3)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
fmt.Printf("Sent: %d\n", i)
}
close(ch)
}()
// 全ての値を受信
for value := range ch {
fmt.Printf("Received: %d\n", value)
}
}
方向性のあるchannel
code: (go)
// 送信専用channel
func sender(ch chan<- int) {
ch <- 100
}
// 受信専用channel
func receiver(ch <-chan int) {
value := <-ch
fmt.Println(value)
}
func main() {
ch := make(chan int)
go sender(ch)
go receiver(ch)
time.Sleep(time.Second)
}
同期と制御パターン
sync.WaitGroup
code: (go)
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("All workers completed")
}
select文による多重化
code: (go)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(time.Second)
ch1 <- "channel 1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "channel 2"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Received from ch2:", msg2)
case <-time.After(3 * time.Second):
fmt.Println("Timeout")
}
}
}
実践的なパターン
Worker Pool パターン
code: (go)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(time.Second)
results <- job * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 3つのworkerを起動
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// ジョブを送信
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
// 結果を受信
for r := 1; r <= 9; r++ {
<-results
}
}
Pub/Sub パターン
code: (go)
type Broker struct {
subscribers mapstring[]chan string mu sync.RWMutex
}
func NewBroker() *Broker {
return &Broker{
subscribers: make(mapstring[]chan string), }
}
func (b *Broker) Subscribe(topic string) <-chan string {
ch := make(chan string, 1)
b.mu.Lock()
b.subscriberstopic = append(b.subscriberstopic, ch) b.mu.Unlock()
return ch
}
func (b *Broker) Publish(topic string, message string) {
b.mu.RLock()
subs := b.subscriberstopic b.mu.RUnlock()
for _, ch := range subs {
select {
case ch <- message:
default:
// バッファが満杯の場合はスキップ
}
}
}
パフォーマンスとベストプラクティス
goroutineリーク対策
code: (go)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
done := make(chan bool)
go func() {
select {
case <-time.After(10 * time.Second):
fmt.Println("Long running task completed")
done <- true
case <-ctx.Done():
fmt.Println("Task cancelled")
return
}
}()
select {
case <-done:
fmt.Println("Task finished normally")
case <-ctx.Done():
fmt.Println("Context timeout")
}
}
runtime パッケージによる監視
code: (go)
func printGoroutineStats() {
fmt.Printf("Goroutines: %d\n", runtime.NumGoroutine())
fmt.Printf("CPUs: %d\n", runtime.NumCPU())
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
}
デバッグとプロファイリング
レースディテクション
code: (bash)
go run -race main.go
goroutineトレース
code: (go)
import (
"os"
"runtime/trace"
)
func main() {
f, _ := os.Create("trace.out")
defer f.Close()
trace.Start(f)
defer trace.Stop()
// アプリケーションコード
}
注意点とよくある間違い
1. channelのcloseし忘れ
code: (go)
// BAD: goroutineリークの可能性
for {
select {
case msg := <-ch:
// 処理
}
}
// GOOD: closeされたchannelは range で検出
for msg := range ch {
// 処理
}
2. 共有状態の不正なアクセス
code: (go)
// BAD: レースコンディション
var counter int
for i := 0; i < 1000; i++ {
go func() {
counter++ // 危険
}()
}
// GOOD: mutexまたはchannelを使用
var mu sync.Mutex
var counter int
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
3. 無制限goroutineの生成
code: (go)
// BAD: リソース枯渇の原因
for req := range requests {
go handleRequest(req)
}
// GOOD: Worker Pool パターンを使用
// (上記のWorker Pool例を参照)
パフォーマンス特性
メモリ使用量
初期スタック: 2KB
最大スタック: 1GB(64bit)/250MB(32bit)
コンテキスト切り替え: ~ns単位(OSスレッド: ~μs単位)
スケジューリング
協調的プリエンプション(Go 1.14以降は非協調的も)
Work-stealing によるロードバランシング
SYSMONによるデッドロック検出と制御
/icons/hr.icon
参考リンク