Go言語入門:よくある質問 -Concurrency Vol.3-

スポンサーリンク
Go言語入門:よくある質問 -Concurrency Vol.3- ノウハウ
Go言語入門:よくある質問 -Concurrency Vol.3-
この記事は約33分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語のよくある質問 について解説しています。

スポンサーリンク

背景

Go言語を学んでいると「なんでこんな仕様になっているんだろう?」「他の言語と違うのはなぜ?」といった疑問が湧いてきませんか。Go言語の公式サイトにあるFAQページには、そんな疑問に対する開発チームからの丁寧な回答がたくさん載っているんです。ただ、英語で書かれているため読むのに少しハードルがあるのも事実で、今回はこのFAQを日本語に翻訳して、Go言語への理解を深めていけたらと思い、これを読んだ時の内容を備忘として残しました。

Concurrency

CPU数をどのように制御できますか?

実行中のgoroutineが同時に使用できるCPU数は、GOMAXPROCSシェル環境変数によって制御され、そのデフォルト値は利用可能なCPUコア数です。並列実行の可能性を持つプログラムは、したがってマルチCPUマシンでデフォルトでそれを達成するべきです。使用する並列CPU数を変更するには、環境変数を設定するか、runtimeパッケージの同様の名前の関数を使用して、異なる数のスレッドを利用するようにランタイムサポートを設定します。これを1に設定すると、真の並列性の可能性を排除し、独立したgoroutineが順番に実行されることを強制します。

ランタイムは複数の未処理I/Oリクエストにサービスを提供するために、GOMAXPROCSの値よりも多くのスレッドを割り当てることができます。GOMAXPROCSは実際に一度に実行できるgoroutineの数のみに影響します。任意により多くのgoroutineがシステムコールでブロックされる可能性があります。

Goのgoroutineスケジューラは、goroutineとスレッドのバランスを取ることに優れており、同じスレッド上の他のものが飢餓状態にならないようにgoroutineの実行を先取りすることさえできます。しかし、それは完璧ではありません。パフォーマンスの問題が見られる場合、アプリケーションごとにGOMAXPROCSを設定することが役立つかもしれません。

解説

この節では、Go言語におけるCPU使用数の制御方法について説明されています。GOMAXPROCSの理解と適切な設定は、並列プログラムのパフォーマンス最適化において重要です。

GOMAXPROCS の基本概念

現在の設定確認と変更

func demonstrateGOMAXPROCS() {
    fmt.Println("GOMAXPROCS の基本操作:")
    
    // 現在の設定を確認
    currentProcs := runtime.GOMAXPROCS(0)  // 0を渡すと現在値を返す
    numCPU := runtime.NumCPU()
    
    fmt.Printf("利用可能CPU数: %d\n", numCPU)
    fmt.Printf("現在のGOMAXPROCS: %d\n", currentProcs)
    
    // 環境変数からの確認
    if envProcs := os.Getenv("GOMAXPROCS"); envProcs != "" {
        fmt.Printf("環境変数GOMAXPROCS: %s\n", envProcs)
    } else {
        fmt.Printf("環境変数GOMAXPROCS: 未設定(デフォルト値使用)\n")
    }
    
    // プログラム実行時の変更
    fmt.Println("\nGOMAXPROCSの動的変更:")
    
    // 1に設定(並列性を無効化)
    oldProcs := runtime.GOMAXPROCS(1)
    fmt.Printf("GOMAXPROCS を %d から 1 に変更\n", oldProcs)
    
    // 並列性のテスト
    testParallelism("GOMAXPROCS=1")
    
    // CPU数に設定
    runtime.GOMAXPROCS(numCPU)
    fmt.Printf("GOMAXPROCS を %d に変更\n", numCPU)
    
    testParallelism(fmt.Sprintf("GOMAXPROCS=%d", numCPU))
    
    // 元の値に戻す
    runtime.GOMAXPROCS(oldProcs)
    fmt.Printf("GOMAXPROCS を元の値 %d に戻す\n", oldProcs)
}

func testParallelism(label string) {
    fmt.Printf("\n%s での並列性テスト:\n", label)
    
    var wg sync.WaitGroup
    numGoroutines := 4
    start := time.Now()
    
    for i := 0; i < numGoroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            // CPU集約的な作業
            sum := 0
            for j := 0; j < 50000000; j++ {
                sum += j
            }
            
            duration := time.Since(start)
            fmt.Printf("  Goroutine %d 完了: %v (sum=%d)\n", id, duration, sum%1000)
        }(i)
    }
    
    wg.Wait()
    totalTime := time.Since(start)
    fmt.Printf("  総実行時間: %v\n", totalTime)
}

環境変数による制御

異なる設定での実行例

# デフォルト(CPU数と同じ)
go run main.go

# 単一CPU使用
GOMAXPROCS=1 go run main.go

# 特定の数のCPU使用
GOMAXPROCS=2 go run main.go

# CPU数より多く設定(通常は効果なし)
GOMAXPROCS=16 go run main.go
func demonstrateEnvironmentControl() {
    fmt.Println("環境変数による制御の例:")
    
    // 現在の設定情報を表示
    showCurrentSettings := func() {
        fmt.Printf("  GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
        fmt.Printf("  NumCPU: %d\n", runtime.NumCPU())
        fmt.Printf("  NumGoroutine: %d\n", runtime.NumGoroutine())
    }
    
    fmt.Println("\n初期設定:")
    showCurrentSettings()
    
    // シミュレーション:異なる GOMAXPROCS 値での動作
    settings := []int{1, 2, 4, runtime.NumCPU()}
    
    for _, procs := range settings {
        if procs > runtime.NumCPU() {
            continue
        }
        
        fmt.Printf("\n--- GOMAXPROCS=%d でのテスト ---\n", procs)
        
        oldProcs := runtime.GOMAXPROCS(procs)
        
        // 並列処理のベンチマーク
        result := benchmarkCPUWork(procs)
        fmt.Printf("実行時間: %v\n", result)
        
        showCurrentSettings()
        
        runtime.GOMAXPROCS(oldProcs) // 元に戻す
    }
}

func benchmarkCPUWork(expectedParallelism int) time.Duration {
    start := time.Now()
    var wg sync.WaitGroup
    
    // CPU集約的なタスクを実行
    numTasks := expectedParallelism * 2 // 並列度の2倍のタスク
    
    for i := 0; i < numTasks; i++ {
        wg.Add(1)
        go func(taskID int) {
            defer wg.Done()
            
            // 計算集約的な処理
            result := 0
            for j := 0; j < 10000000; j++ {
                result += j * j % 1000
            }
            
            // 結果を使用(最適化による削除を防ぐ)
            _ = result
        }(i)
    }
    
    wg.Wait()
    return time.Since(start)
}

I/O バウンド vs CPU バウンドの影響

GOMAXPROCS の異なる影響

func demonstrateIOvsCPUBound() {
    fmt.Println("I/OバウンドとCPUバウンドでのGOMAXPROCSの影響:")
    
    // CPU バウンドなタスク
    cpuBoundTask := func(numGoroutines int) time.Duration {
        start := time.Now()
        var wg sync.WaitGroup
        
        for i := 0; i < numGoroutines; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                
                // CPU集約的な処理
                sum := 0.0
                for j := 0; j < 1000000; j++ {
                    sum += math.Sin(float64(j)) * math.Cos(float64(j))
                }
                _ = sum
            }()
        }
        
        wg.Wait()
        return time.Since(start)
    }
    
    // I/O バウンドなタスク
    ioBoundTask := func(numGoroutines int) time.Duration {
        start := time.Now()
        var wg sync.WaitGroup
        
        for i := 0; i < numGoroutines; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                
                // I/O集約的な処理(スリープでシミュレート)
                time.Sleep(10 * time.Millisecond)
            }()
        }
        
        wg.Wait()
        return time.Since(start)
    }
    
    numCPU := runtime.NumCPU()
    
    // 異なる GOMAXPROCS 設定でテスト
    for _, procs := range []int{1, numCPU/2, numCPU, numCPU * 2} {
        if procs <= 0 {
            continue
        }
        
        fmt.Printf("\n--- GOMAXPROCS=%d ---\n", procs)
        
        oldProcs := runtime.GOMAXPROCS(procs)
        
        // CPUバウンドタスクのテスト
        cpuTime := cpuBoundTask(numCPU)
        fmt.Printf("CPUバウンド (%d goroutines): %v\n", numCPU, cpuTime)
        
        // I/Oバウンドタスクのテスト
        ioTime := ioBoundTask(100) // より多くのgoroutines
        fmt.Printf("I/Oバウンド (100 goroutines): %v\n", ioTime)
        
        runtime.GOMAXPROCS(oldProcs)
    }
    
    fmt.Println("\n観察ポイント:")
    fmt.Println("• CPUバウンド: GOMAXPROCS=CPU数で最適")
    fmt.Println("• I/Oバウンド: GOMAXPROCSの影響は限定的")
    fmt.Println("• I/O待機中は他のgoroutineが実行される")
}

実際のスレッド数とGOMAXPROCSの関係

スレッド使用量の監視

func demonstrateThreadUsage() {
    fmt.Println("実際のスレッド使用量の監視:")
    
    // スレッド数を監視する関数
    monitorThreads := func(label string, duration time.Duration) {
        fmt.Printf("\n--- %s ---\n", label)
        
        ticker := time.NewTicker(100 * time.Millisecond)
        defer ticker.Stop()
        
        done := time.After(duration)
        
        for {
            select {
            case <-ticker.C:
                var m runtime.MemStats
                runtime.ReadMemStats(&m)
                
                fmt.Printf("Goroutines: %d, Threads: %d, GOMAXPROCS: %d\n",
                    runtime.NumGoroutine(),
                    runtime.NumGoroutine(), // 近似値として使用
                    runtime.GOMAXPROCS(0))
                    
            case <-done:
                return
            }
        }
    }
    
    // 1. CPU集約的なワークロード
    go func() {
        var wg sync.WaitGroup
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                for j := 0; j < 100000000; j++ {
                    _ = j * j
                }
            }()
        }
        wg.Wait()
    }()
    
    monitorThreads("CPU集約的ワークロード", 2*time.Second)
    
    // 2. I/O集約的なワークロード
    go func() {
        var wg sync.WaitGroup
        for i := 0; i < 100; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                time.Sleep(1 * time.Second)
            }()
        }
        wg.Wait()
    }()
    
    monitorThreads("I/O集約的ワークロード", 2*time.Second)
    
    // 3. ブロッキングシステムコール
    go func() {
        var wg sync.WaitGroup
        for i := 0; i < 20; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                // ファイルI/O(ブロッキング)
                data := make([]byte, 1024)
                for j := 0; j < 10; j++ {
                    file, err := os.CreateTemp("", "test")
                    if err == nil {
                        file.Write(data)
                        file.Close()
                        os.Remove(file.Name())
                    }
                    time.Sleep(100 * time.Millisecond)
                }
            }()
        }
        wg.Wait()
    }()
    
    monitorThreads("ブロッキングI/O", 3*time.Second)
}

パフォーマンス最適化の実例

アプリケーション別の最適設定

func demonstrateOptimizationExamples() {
    fmt.Println("アプリケーション別GOMAXPROCS最適化:")
    
    // 1. Webサーバー型アプリケーション
    webServerOptimization := func() {
        fmt.Println("\n1. Webサーバー型アプリケーション:")
        
        // リクエスト処理のシミュレーション
        handleRequest := func(requestID int, responseTime time.Duration) {
            // データベースアクセスのシミュレーション
            time.Sleep(responseTime)
            
            // CPUでの処理
            result := 0
            for i := 0; i < 100000; i++ {
                result += i
            }
        }
        
        testWebServer := func(gomaxprocs int, concurrent int) time.Duration {
            oldProcs := runtime.GOMAXPROCS(gomaxprocs)
            defer runtime.GOMAXPROCS(oldProcs)
            
            start := time.Now()
            var wg sync.WaitGroup
            
            for i := 0; i < concurrent; i++ {
                wg.Add(1)
                go func(id int) {
                    defer wg.Done()
                    handleRequest(id, 50*time.Millisecond) // 50ms のI/O
                }(i)
            }
            
            wg.Wait()
            return time.Since(start)
        }
        
        concurrentRequests := 100
        numCPU := runtime.NumCPU()
        
        for _, procs := range []int{1, numCPU, numCPU * 2} {
            duration := testWebServer(procs, concurrentRequests)
            fmt.Printf("  GOMAXPROCS=%d: %v (%d concurrent requests)\n", 
                       procs, duration, concurrentRequests)
        }
        
        fmt.Println("  推奨: GOMAXPROCS=CPU数(I/O待機中に他のリクエストを処理)")
    }
    
    // 2. 計算集約型アプリケーション
    computeIntensiveOptimization := func() {
        fmt.Println("\n2. 計算集約型アプリケーション:")
        
        // 大きな行列の乗算シミュレーション
        matrixMultiply := func(size int) {
            a := make([][]float64, size)
            b := make([][]float64, size)
            c := make([][]float64, size)
            
            for i := range a {
                a[i] = make([]float64, size)
                b[i] = make([]float64, size)
                c[i] = make([]float64, size)
                for j := range a[i] {
                    a[i][j] = rand.Float64()
                    b[i][j] = rand.Float64()
                }
            }
            
            // 行列乗算
            for i := 0; i < size; i++ {
                for j := 0; j < size; j++ {
                    for k := 0; k < size; k++ {
                        c[i][j] += a[i][k] * b[k][j]
                    }
                }
            }
        }
        
        testComputeIntensive := func(gomaxprocs int) time.Duration {
            oldProcs := runtime.GOMAXPROCS(gomaxprocs)
            defer runtime.GOMAXPROCS(oldProcs)
            
            start := time.Now()
            var wg sync.WaitGroup
            
            numTasks := runtime.NumCPU()
            for i := 0; i < numTasks; i++ {
                wg.Add(1)
                go func() {
                    defer wg.Done()
                    matrixMultiply(100) // 100x100 行列
                }()
            }
            
            wg.Wait()
            return time.Since(start)
        }
        
        numCPU := runtime.NumCPU()
        
        for _, procs := range []int{1, numCPU/2, numCPU, numCPU * 2} {
            if procs <= 0 {
                continue
            }
            duration := testComputeIntensive(procs)
            fmt.Printf("  GOMAXPROCS=%d: %v\n", procs, duration)
        }
        
        fmt.Println("  推奨: GOMAXPROCS=CPU数(CPU集約的な処理)")
    }
    
    // 3. バッチ処理アプリケーション
    batchProcessingOptimization := func() {
        fmt.Println("\n3. バッチ処理アプリケーション:")
        
        processData := func(data []int) int {
            sum := 0
            for _, v := range data {
                // 複雑な処理のシミュレーション
                for i := 0; i < 1000; i++ {
                    sum += v * i
                }
            }
            return sum
        }
        
        testBatchProcessing := func(gomaxprocs int) time.Duration {
            oldProcs := runtime.GOMAXPROCS(gomaxprocs)
            defer runtime.GOMAXPROCS(oldProcs)
            
            start := time.Now()
            var wg sync.WaitGroup
            
            // 大量のデータを分割して処理
            totalData := 10000
            chunkSize := totalData / gomaxprocs
            
            for i := 0; i < gomaxprocs; i++ {
                wg.Add(1)
                go func(start, end int) {
                    defer wg.Done()
                    
                    chunk := make([]int, end-start)
                    for j := range chunk {
                        chunk[j] = start + j
                    }
                    
                    _ = processData(chunk)
                }(i*chunkSize, (i+1)*chunkSize)
            }
            
            wg.Wait()
            return time.Since(start)
        }
        
        numCPU := runtime.NumCPU()
        
        for _, procs := range []int{1, numCPU/2, numCPU, numCPU * 2} {
            if procs <= 0 {
                continue
            }
            duration := testBatchProcessing(procs)
            fmt.Printf("  GOMAXPROCS=%d: %v\n", procs, duration)
        }
        
        fmt.Println("  推奨: GOMAXPROCS=CPU数(データ並列処理)")
    }
    
    webServerOptimization()
    computeIntensiveOptimization()
    batchProcessingOptimization()
}

動的な GOMAXPROCS 調整

実行時の動的調整

func demonstrateDynamicAdjustment() {
    fmt.Println("動的なGOMAXPROCS調整:")
    
    // システム負荷に基づく調整
    type LoadMonitor struct {
        mu           sync.RWMutex
        cpuUsage     float64
        goroutines   int
        optimalProcs int
    }
    
    func NewLoadMonitor() *LoadMonitor {
        return &LoadMonitor{
            optimalProcs: runtime.NumCPU(),
        }
    }
    
    func (lm *LoadMonitor) UpdateMetrics() {
        lm.mu.Lock()
        defer lm.mu.Unlock()
        
        // 簡単なメトリクス収集(実際にはより詳細な監視が必要)
        lm.goroutines = runtime.NumGoroutine()
        
        // CPU使用率のシミュレーション(実際はシステムメトリクスを使用)
        lm.cpuUsage = rand.Float64() * 100
    }
    
    func (lm *LoadMonitor) AdjustGOMAXPROCS() {
        lm.mu.RLock()
        defer lm.mu.RUnlock()
        
        current := runtime.GOMAXPROCS(0)
        newProcs := current
        
        // 簡単な調整ロジック
        if lm.cpuUsage > 80 && lm.goroutines > current*10 {
            // 高CPU使用率 + 多数のgoroutine → 削減
            newProcs = max(1, current-1)
        } else if lm.cpuUsage < 50 && lm.goroutines < current*5 {
            // 低CPU使用率 + 少数のgoroutine → 増加
            newProcs = min(runtime.NumCPU(), current+1)
        }
        
        if newProcs != current {
            runtime.GOMAXPROCS(newProcs)
            fmt.Printf("GOMAXPROCS adjusted: %d -> %d (CPU: %.1f%%, Goroutines: %d)\n",
                       current, newProcs, lm.cpuUsage, lm.goroutines)
        }
    }
    
    monitor := NewLoadMonitor()
    
    // 動的調整のシミュレーション
    go func() {
        ticker := time.NewTicker(500 * time.Millisecond)
        defer ticker.Stop()
        
        for i := 0; i < 10; i++ {
            <-ticker.C
            monitor.UpdateMetrics()
            monitor.AdjustGOMAXPROCS()
        }
    }()
    
    // 変動するワークロードの生成
    var wg sync.WaitGroup
    for phase := 0; phase < 3; phase++ {
        fmt.Printf("\nPhase %d: ", phase+1)
        
        var numGoroutines int
        var workIntensity int
        
        switch phase {
        case 0:
            fmt.Println("軽いワークロード")
            numGoroutines = 5
            workIntensity = 1000000
        case 1:
            fmt.Println("重いワークロード")
            numGoroutines = 20
            workIntensity = 10000000
        case 2:
            fmt.Println("中程度のワークロード")
            numGoroutines = 10
            workIntensity = 5000000
        }
        
        for i := 0; i < numGoroutines; i++ {
            wg.Add(1)
            go func(intensity int) {
                defer wg.Done()
                sum := 0
                for j := 0; j < intensity; j++ {
                    sum += j
                }
                _ = sum
            }(workIntensity)
        }
        
        time.Sleep(2 * time.Second)
    }
    
    wg.Wait()
    
    fmt.Printf("\n最終GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

ベストプラクティス

GOMAXPROCS設定の指針

func demonstrateBestPractices() {
    fmt.Println("GOMAXPROCS設定のベストプラクティス:")
    
    practices := []struct {
        scenario string
        setting  string
        reason   string
    }{
        {
            "CPU集約的アプリケーション",
            "GOMAXPROCS = CPU数",
            "各CPUコアで並列実行し最大性能を発揮",
        },
        {
            "I/O集約的アプリケーション",
            "GOMAXPROCS = CPU数(デフォルト)",
            "I/O待機中に他のgoroutineが実行される",
        },
        {
            "混合ワークロード",
            "GOMAXPROCS = CPU数",
            "バランスの取れた設定として適切",
        },
        {
            "コンテナ環境",
            "CPUクォータに基づく設定",
            "利用可能リソースに合わせた制限",
        },
        {
            "デバッグ・開発",
            "GOMAXPROCS = 1",
            "並行性問題を特定しやすくする",
        },
        {
            "レガシーシステム",
            "段階的に増加",
            "既存システムへの影響を最小化",
        },
    }
    
    for _, practice := range practices {
        fmt.Printf("\n%s:\n", practice.scenario)
        fmt.Printf("  設定: %s\n", practice.setting)
        fmt.Printf("  理由: %s\n", practice.reason)
    }
    
    fmt.Println("\n一般的な注意点:")
    warnings := []string{
        "• CPU数より大幅に多く設定しても効果なし",
        "• 1に設定すると並列性が完全に無効化",
        "• コンテナでは利用可能CPU数を正確に把握",
        "• ベンチマークで実際の効果を測定",
        "• アプリケーションの特性を理解してから設定",
    }
    
    for _, warning := range warnings {
        fmt.Println(warning)
    }
    
    // 現在の推奨設定
    fmt.Printf("\n現在の環境での推奨設定:\n")
    fmt.Printf("  CPU数: %d\n", runtime.NumCPU())
    fmt.Printf("  現在のGOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
    fmt.Printf("  推奨設定: GOMAXPROCS=%d (デフォルトのまま)\n", runtime.NumCPU())
}

Go言語におけるGOMAXPROCSの制御は、アプリケーションの性質を理解した上で適切に設定することが重要です。多くの場合、デフォルト値(CPU数と同じ)が最適ですが、特定の要件や制約がある場合は調整を検討する価値があります。

おわりに 

本日は、Go言語のよくある質問について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました