LoginSignup
4
0

goroutine、それでいつ使うのか

Last updated at Posted at 2023-11-30

概要

ゴルチンは、Go言語で提供される並列実行のためのスレッドライブラリです。
Go言語の基本的な機能で、他の言語のスレッドに似た役割を果たします。
ゴルチンだけのいい点は

  • 軽い
  • 同じOSスレッドふくすう実行できる - 同じ環境でより多くの並列性を提供
  • 便利なchannel機能
  • ランタイムスケジューリング

要約すると、スケジューリングや並列処理後のデータ収集と関連して、
ユーザーが気を使わなければならない部分が少なく、
軽いうえに強力であることもあり、超便利だということです。
しかし、このような長所を享受するためには次が前提されなければなりません。

  • ゴルチン間のデータ共有時にチャネルを使用して安全に実行します。
  • ゴルチンを無分別に生成しないように注意します。
  • pprofを使用して、ゴルチンのパフォーマンスを監視します。

上記の注意点を常に肝に銘じて使いましょう

使い方

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func main() {

    // ゴルチンの性能を評価
    // cpu profile内容を記載するファイルを作成します。
    cpuProfile, err := os.Create("cpu.pprof")
    if err != nil {
        fmt.Println("Error creating CPU profile:", err)
        return
    }
    defer cpuProfile.Close()

    // cpu profile開始、この下からCPU使用率を確認します
    pprof.StartCPUProfile(cpuProfile)
    defer pprof.StopCPUProfile()

    // ゴルチンで実行される処理のアウトプットを収集するためのチャネル
    ch := make(chan int)
    
    // 10個の処理を並列処理
    for i := 0; i < 10; i++ {
        go func(id int) {
          // ここでやりたいをします
          ch <- id // ここでアウトプットをチャンネルに入れます
        }(i)
    }

    // チャンネルで収集したアウトプットを整理
    for i := 0; i < 10; i++ {
        fmt.Println(<-ch)
    }
    
    // memory profile内容を記載するファイルを作成します。
    memProfile, err := os.Create("mem.pprof")
    if err != nil {
        fmt.Println("Error creating memory profile:", err)
        return
    }
    defer memProfile.Close()
    
    // memory profile書きます
    pprof.WriteHeapProfile(memProfile)
}

実行結果

0
1
2
3
4
5
6
7
8
9

pprofを使用してゴルチンの性能をモニタリングするには、次のようにします。

go tool pprof cpu.pprof
go tool pprof mem.pprof

この画面で各コルーチンの実行時間、CPU使用率、メモリ使用量などを確認することができます。

それでいつ使うのか

主にはI/O処理です。
ゴルチンを使った方が得な場合は、以下の3つがあります。

  • CPU集約的な作業がある場合
  • 長時間実行される場合
  • 同期問題を解決する必要がある場合

大規模データを複数のリソースから読み書きするのは、3つのケースすべてに当てはまります。
絶対ゴルチンを使えば得をする! という場合は例を挙げにくいのですが、上記のような場合は書いて損をすることはありません。

下記は例です。

package main

import (
	"fmt"
	"io/ioutil"
	"sync"
)

const (
	inputFileName  = "input.txt"
	outputFileName = "output"
	numTasks       = 10
	numWorkers     = 4
)

type Task struct {
	ID int
}

type Result struct {
	WorkerID int
	TaskID   int
	Status   string
}

func worker(id int, tasks <-chan Task, results chan<- Result, wg *sync.WaitGroup) {
	defer wg.Done()

	// tasksがcloseされるまでにloopする
	for task := range tasks {
		// 読み
		inputData, err := ioutil.ReadFile(inputFileName)
		if err != nil {
			results <- Result{WorkerID: id, TaskID: task.ID, Status: "Error reading input file"}
			continue
		}

		// 書き
		err = ioutil.WriteFile(fmt.Sprintf("%s%d-%d.txt", outputFileName, id, task.ID), inputData, 0644)

		if err != nil {
			results <- Result{WorkerID: id, TaskID: task.ID, Status: "Error writing output file"}
			continue
		}

		results <- Result{WorkerID: id, TaskID: task.ID, Status: "Completed"}
		fmt.Printf("Worker %d's Task %d done\n", id, task.ID)
	}
}

func main() {
	// Read処理対象作成
	inputData := []byte("Hello, World!")
	err := ioutil.WriteFile(inputFileName, inputData, 0644)
	if err != nil {
		fmt.Println("Error generating input file:", err)
		return
	}

	// 各処理を管理するためのChannelと処理結果Collection用Channel
	tasks := make(chan Task, numWorkers)
	results := make(chan Result, numWorkers)

	// すべてのwgが完了されることを待つ
	var wg sync.WaitGroup

	// 並列処理開始
	for i := 1; i <= numWorkers; i++ {
		// 待ち対象を増やす
		wg.Add(1)
		go worker(i, tasks, results, &wg)
	}

	// worker処理を待つ
	go func() {
		wg.Wait()
		close(results)
	}()

	// 各workerに作業を割り当てる
	go func() {
		for i := 1; i <= numTasks; i++ {
			task := Task{ID: i}
			tasks <- task
		}
		close(tasks)
	}()

	// resultsにcollectされた結果を出力する
	for result := range results {
		fmt.Printf("Result for Task %d: %s\n", result.TaskID, result.Status)
	}
}

	}
}

上記コードの中、wgインスタンスを生成する場所からの流れを説明します。

  1. worker関数がWait Groupに登録され、それぞれのゴールチンが始まります。
  2. メイン関数は、Wait Groupが0になるまで(wg.Done()がwg.Addで増えた数量分実行される)ブロックされ、worker関数が作業を完了するたびに内部カウンターが減少します。
  3. go func() では、ジョブを生成し、tasksチャネルを閉じます。(Callされた各worker関数はfor task := range tasks loopがtasksがcloseされるまでloopします)しかし、このコードはmain関数がWait Groupを通じて待機している時だけ実行されます。
  4. メイン関数のWait Group待機が解除されると、go func()での作業生成とチャネル閉じるコードが実行されますが、すでにすべてのworker関数が作業を受けてきたため、影響を与えません。

これにより、大規模データのI/O処理で同期問題を解決し、パフォーマンスを向上させることができます。

ゴルチンを使用すると、大規模データのI/O処理で次のような利点が得られます。

  • 同期問題解決
  • 性能向上
  • コードの簡潔さ

I/O以外にも、最初に言及した3つの場合は、ゴルチンを使って上記の利点を得ることができるかどうかを考慮することをお勧めします。(処理の間に待機時間が存在するネットワーク処理などがあります。)

ゴルチンは、複数のOSスレッドの上に複数のゴルチンを回します。コンテキストスイッチの速度も速く、マルチコアも活用できます。実装が難しいかったことを、言語から提供することで楽させてます。

しかし、無分別に使用すると、従来のスレッドのようにむしろ性能に悪影響を及ぼす可能性があります。 いつ、どのように使うべきか、長所と短所を把握して使わなければなりません。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0