1 ゴルーチンの概要
1.1 並行性と並列性の基本概念
並行性と並列性は、マルチスレッドプログラミングにおいてよく使われる2つの概念です。これらは、同時に発生する可能性のあるイベントやプログラムの実行を表現するために使用されます。
- 並行性 は、複数のタスクが同時に処理されることを指し、ただ1つのタスクが任意の時点で実行されています。タスクは迅速に切り替わり、ユーザーには同時実行の錯覚が与えられます。並行性はシングルコアプロセッサに適しています。
- 並列性 は、複数のタスクが実際に同時に同時実行されることを指し、これにはマルチコアプロセッサのサポートが必要です。
Go言語は、その主要な目標の1つとして並行性を考慮して設計されています。これは、ゴルーチン(Goroutines)とチャネルを通じて効率的な並行プログラミングモデルを実現しています。Goのランタイムがゴルーチンを管理し、これらのゴルーチンを複数のシステムスレッドでスケジューリングして、並列処理を実現します。
1.2 Go言語におけるゴルーチン
ゴルーチンは、Go言語における並行プログラミングを実現するための中核概念です。これらは、Goのランタイムによって管理される軽量スレッドです。ユーザーの視点からはスレッドに類似していますが、より少ないリソースを消費し、より迅速に開始します。
ゴルーチンの特性は次のとおりです:
- 軽量性:ゴルーチンは従来のスレッドよりも少ないスタックメモリを占有し、スタックサイズは必要に応じて動的に拡張または縮小することができます。
- 低オーバーヘッド:ゴルーチンの生成と破棄にかかるオーバーヘッドは、従来のスレッドよりも低くなっています。
- シンプルな通信メカニズム:チャネルはゴルーチン間のシンプルかつ効果的な通信メカニズムを提供します。
- 非同期設計:あるゴルーチンがI/O処理を待っている間、他のゴルーチンは実行を継続することができます。
2 ゴルーチンの作成と管理
2.1 ゴルーチンの作成方法
Go言語では、go
キーワードを使用して簡単にゴルーチンを作成できます。関数呼び出しにgo
キーワードを付けると、その関数は新しいゴルーチンで非同期に実行されます。
以下は簡単な例です:
package main
import (
"fmt"
"time"
)
// Helloを出力する関数を定義
func sayHello() {
fmt.Println("Hello")
}
func main() {
// goキーワードを使用して新しいゴルーチンを開始
go sayHello()
// メインのゴルーチンは、sayHelloの実行を許可するために一定時間待ちます
time.Sleep(1 * time.Second)
fmt.Println("Main function")
}
上記のコードでは、sayHello()
関数が新しいゴルーチンで非同期に実行されます。つまり、main()
関数はsayHello()
の完了を待たずに継続します。そのため、time.Sleep
を使用してメインのゴルーチンを一時停止させ、sayHello
のプリント文が実行されるようにしています。これはデモンストレーション目的のみです。実際の開発では、通常、チャネルや他の同期方法を使用して、異なるゴルーチンの実行を調整します。
注意: 実際のアプリケーションでは、ゴルーチンの完了を待つために
time.Sleep()
を使用するべきではありません。これは信頼性のない同期メカニズムです。
2.2 ゴルーチンのスケジューリングメカニズム
Go言語では、ゴルーチンのスケジューリングはGoランタイムのスケジューラによって処理され、利用可能な論理プロセッサ上での実行時間の割り当てを担当しています。GoスケジューラはM:N
スケジューリングテクノロジー(複数のゴルーチンが複数のOSスレッドにマップされる)を使用して、マルチコアプロセッサ上でより良いパフォーマンスを実現しています。
GOMAXPROCSと論理プロセッサ
GOMAXPROCS
は、ランタイムスケジューラに利用可能なCPUの最大数を定義する環境変数であり、デフォルト値はマシンのCPUコア数です。Goランタイムは、各論理プロセッサに1つのOSスレッドを割り当てます。GOMAXPROCSを設定することで、ランタイムが使用するコアの数を制限することができます。
import "runtime"
func init() {
runtime.GOMAXPROCS(2)
}
上記のコードは、プログラムを複数のコアで実行しているマシンであっても、ゴルーチンをスケジューリングするために最大2つのコアを設定しています。
スケジューラの動作
スケジューラは、M (マシン)、P (プロセッサ)、およびG (ゴルーチン) という3つの重要なエンティティを使用して動作します。M はマシンやスレッドを表し、OS カーネルスレッドの抽象化として機能します。P はゴルーチンの実行に必要なリソースを表し、各P にはローカルゴルーチンキューがあります。G はゴルーチンを表し、その実行スタック、命令セット、およびその他の情報を含みます。
Go のスケジューラの動作原則は次のとおりです:
- M はG を実行するためにP を持たなければなりません。P がない場合、M はスレッドキャッシュに戻されます。
- G が他のG によってブロックされていないとき(たとえば、システムコール中ではないとき)、それは可能な限り同じM で実行され、より効率的なCPU キャッシュ利用のためにG のローカルデータを「ホット」に保ちます。
- G がブロックされると、M とP は分離し、P は新しいM を探したり、新しいM を起動して他のG にサービスを提供します。
go func() {
fmt.Println("ゴルーチンからこんにちは")
}()
上記のコードは新しいゴルーチンを起動し、これによりスケジューラはこの新しいG を実行のためのキューに追加することを示しています。
ゴルーチンの優先スケジューリング
初期段階では、Go では協調スケジューリングが使用されており、これはゴルーチンが自主的に制御を放棄せずに長時間実行されると、他のゴルーチンをスターブさせる可能性がありました。現在、Go スケジューラは、他のG に実行の機会を与えるために、長時間実行されるG を一時停止する優先スケジューリングを実装しています。
2.3 ゴルーチンのライフサイクル管理
Go アプリケーションの堅牢性とパフォーマンスを確保するためには、ゴルーチンのライフサイクルを理解し適切に管理することが重要です。ゴルーチンの開始は簡単ですが、適切な管理がないと、メモリリークや競合状態などの問題を引き起こす可能性があります。
ゴルーチンの安全な開始
ゴルーチンを開始する前に、その作業量とランタイムの特性を理解してください。ゴルーチンは明確な開始と終了を持つべきであり、「ゴルーチン孤児」が終了条件なしに作成されないようにする必要があります。
func worker(done chan bool) {
fmt.Println("作業中...")
time.Sleep(time.Second) // 高コストなタスクのシミュレーション
fmt.Println("作業完了.")
done <- true
}
func main() {
// ここでは、Go のチャネルメカニズムを使用しています。チャネルを基本的なメッセージキューと考え、"<-" 演算子を使用してキューデータを読み書きします。
done := make(chan bool, 1)
go worker(done)
// ゴルーチンの終了を待ちます
<-done
}
上記のコードは、done
チャネルを使用してゴルーチンの終了を待つ方法を示しています。
注意: この例では、後の章で詳しく説明しますが、Go のチャネルメカニズムを使用しています。
ゴルーチンの停止
一般的に、プログラム全体の終了は暗黙的にすべてのゴルーチンを終了します。ただし、長時間実行されるサービスでは、アクティブにゴルーチンを停止する必要がある場合があります。
- チャネルを使用して停止信号を送信: ゴルーチンはチャネルをポーリングして停止信号を確認できます。
stop := make(chan struct{})
go func() {
for {
select {
case <-stop:
fmt.Println("停止信号を受信しました。シャットダウン中...")
return
default:
// 通常の操作を実行
}
}
}()
// 停止信号を送信
stop <- struct{}{}
-
context
パッケージを使用してライフサイクルを管理:
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("停止信号を受信しました。シャットダウン中...")
return
default:
// 通常の操作を実行
}
}
}(ctx)
// ゴルーチンを停止したい時
cancel()
context
パッケージの使用により、より柔軟なゴルーチンの制御が可能となり、タイムアウトやキャンセルの機能を提供します。大規模なアプリケーションやマイクロサービスでは、context
がゴルーチンのライフサイクルを制御する推奨される方法です。