Go言語のゴルーチンについて解説

Go_icon Go
この記事は約7分で読めます。

この記事は以下の方を対象に書いています。

  • ゴルーチンの基礎的な概念を知りたい
  • サンプルコードを見たい

ゴルーチンとは?

ゴルーチン(Goroutine)とは、Go言語の並行性と並列性を実現するための要素で、

軽量なスレッドのようなものです。

通常、プログラム内で複数のゴルーチンが同時に実行され、非同期処理や並行性を実現します。

ゴルーチンは、スタックやコンテキストの管理など、スレッドよりも遥かに軽量で効率的です。

ゴルーチンの作成

以下のように、「go」キーワードを使用して簡単に作成することができます。

go func() {
  // ゴルーチンで実行する処理
}()

ゴルーチンの中では、一般的に次のような処理を記述することが多いです。

バックグラウンドでのデータ処理データベースからデータを取得し、
それを加工したり処理したりするタスクは、
ゴルーチンを使用して非同期に実行するのに適しています。
これによって、メインプログラムのブロッキングを回避できます。
ネットワーク通信インターネット上のリモートサーバーと通信する際に、
ゴルーチンを使用して非同期でリクエストを送信し、
レスポンスを受け取ることができます。
これによって、ネットワークの待ち時間に効果的に対処できます。
ファイルI/Oファイルの読み書きやログの書き込みなど、
ディスクI/O操作はゴルーチンを使用して効率的に行うことができます。
ゴルーチンを使ってファイルI/Oを行うことによって、
ディスクアクセスの待ち時間を他のタスクに利用できます。
長時間かかる計算CPUバウンドなタスクや数学的な計算など、
計算に時間がかかるタスクもゴルーチンを使用して
非同期に処理できます。
これによって、アプリケーションの応答性が向上します。
ゴルーチン内でよく使用される処理

ゴルーチン間の通信

一般的には、チャネル(Channel)を使用してデータをやり取りし、同期を行います。

チャネルを介してデータを送受信することで、ゴルーチン間の相互作用を安全に行うことができます。

チャネルについては、以下の記事で解説しています。

ゴルーチンのスケジューリング

スケジューリングは、Goプログラムのランタイム(runtime)が

管理するプロセスで、Go言語の並行性と並列性を実現する重要な要素です。

以下は、スケジューリングに関する詳細情報です。

マルチプレキシング

ゴルーチンのスケジューリングは、Goランタイムによって行われます。

ランタイムは、実行可能なゴルーチンが複数ある場合、

それらを効率的にスケジュールし、実行するための仕組みを提供します。

これをマルチプレキシングと呼びます。

GOMAXPROCS

「GOMAXPROCS」環境変数を使用して、

Goプログラムが同時に実行するゴルーチンの数を制御することができます。

デフォルトでは、CPUのコア数に設定されます。

これによって、マルチコアCPUを効率よく活用することができます。

コンテキストの切替

ゴルーチンの切り替えは、Goランタイムによって自動的に行われます。

ゴルーチンがシステムコール(例: ファイルI/O、ネットワーク通信)などの

ブロッキング操作を行うと、Goランタイムはそのゴルーチンを一時停止し、

他のゴルーチンを実行します。

これによって、待ち時間を最小限に抑えつつ、他のゴルーチンを進行させることができます。

ゴルーチンの実行キュー

ゴルーチンは実行待ちのキューに配置されます。

ランタイムは、キュー内のゴルーチンを適切にスケジュールして実行します。

優先順位のあるキューも存在し、重要なタスクに対して優先的に実行されるようになります。

スケジューラの仕組み

Goランタイムには、ユーザーが直接制御できるスケジューラはありません。

スケジューラの詳細な実装は内部で行われ、ユーザーからは隠蔽されています。

これによって、プログラムはスレッドの管理などの低レベルな処理から解放されます。

ゴルーチンの同期

ゴルーチンの同期は、「sync」パッケージに含まれる

Mutex」や「WaitGroup」などを使用します。

これらのツールを使用することで、複数のゴルーチンが協調的に動作できます。

サンプルコード

以下は、ゴルーチンの同期を実現するための簡単なサンプルコードです。

このコードでは、2つのゴルーチンが協調して数値を増加させています。

package main

import (
	"fmt"
	"sync"
)

func main() {
	// WaitGroupを作成し、2つのゴルーチンの完了を待つ
	var wg sync.WaitGroup

	// ゴルーチン1: 数値を増加させる
	wg.Add(1)
	go increment(&wg)

	// ゴルーチン2: 数値を増加させる
	wg.Add(1)
	go increment(&wg)

	// すべてのゴルーチンの完了を待つ
	wg.Wait()

	fmt.Println("処理が完了しました")
}

func increment(wg *sync.WaitGroup) {
	defer wg.Done() // ゴルーチンの完了を通知するためにdeferを使用

	for i := 0; i < 5; i++ {
		fmt.Println("数値を増加:", i)
	}
}

// 数値を増加: 0
// 数値を増加: 1
// 数値を増加: 2
// 数値を増加: 3
// 数値を増加: 4
// 数値を増加: 0
// 数値を増加: 1
// 数値を増加: 2
// 数値を増加: 3
// 数値を増加: 4
// 処理が完了しました

Addメソッドの引数は、待機中のゴルーチンを増やすための数字です。

Doneメソッドは、待機中のゴルーチンを減らします。

レースコンディションの防止

レースコンディション(Race Condition)は、

複数のスレッドやゴルーチンが同時に共有されるリソースにアクセスし、

その結果が予測不能な振る舞いを引き起こす可能性がある状態のことです。

レースコンディションは並行プログラムのバグの主要な原因の一つであり、

データ競合や不正な状態を引き起こす可能性があるため、適切に防止する必要があります。

以下は、レースコンディションを防止するための一般的な方法です。

ミューテックス(Mutex)を使用

ミューテックスは、並行プログラムにおいて共有リソースにアクセスするための排他制御手法です。

複数のスレッドやゴルーチンが同時にミューテックスを取得しようとすると、

そのうちの一つしか成功せず、他のスレッドやゴルーチンは待機します。

これにより、共有リソースへのアクセスが単一スレッドまたはゴルーチンによって

順番に行われるため、競合状態を回避できます。

チャネル(Channel)を使用

チャネルは、ゴルーチン間のデータの安全な受け渡しと同期を可能にします。

ゴルーチンはデータをチャネルに送信する前に、チャネルが利用可能であることを

確認するために待機し、受信側もデータが届くまで待機します。

これによって、競合状態を回避し、データの一貫性を確保できます。

アトミック操作を使用

アトミック操作は、特定の操作(例: インクリメント、デクリメント、比較交換)を

一つの命令として実行する方法です。アトミック操作を使用することで、

競合状態を避けつつ、共有変数の値を変更できます。

Go言語では、「atomic」パッケージを使用します。

ロックフリーなデータ構造を選択

ロックフリーなデータ構造は、

スレッドやゴルーチン間の競合を最小限に抑えるために設計されています。

これらのデータ構造を選択することで、高性能で競合が発生しにくいプログラムを実現できます。

不変データ構造を使用

不変データ構造は、一度生成されたら変更できないデータを表現する方法です。

これによって、競合状態を回避しやすくなります。

最後に

最後まで読んでいただきありがとうございます。

今回は、ゴルーチンについて解説しました。

「ここは間違ってる」等のご意見ありましたら、コメントしていただくと幸いです。

コメント

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