ゴルーチンの基本
Goroutine
Gopherの方々にGo言語の利点をお話しする際、頻繁に登場する**並行性 (Concurrency)に関する記述があります。その基盤となっているのは、軽量で簡潔に処理できるゴルーチン (goroutine)**です。これについて簡潔に記述しました。
並行性 (Concurrency) vs 並列性 (Parallelism)
ゴルーチンを理解する前に、しばしば混同される2つの概念について先に説明します。
- 並行性: 並行性とは、多くのタスクを一度に処理することに関するものです。必ずしも実際に同時に実行されるという意味ではなく、複数のタスクを小さな単位に分割し、交互に実行することで、ユーザーからは複数のタスクが同時に処理されているように見せる構造的、論理的な概念です。シングルコアでも並行性は可能です。
- 並列性: 並列性とは、「複数のコアで複数のタスクを同時に処理すること」です。文字通り並列にタスクを進め、異なるタスクを同時に実行します。
ゴルーチンはGoランタイムスケジューラを通じて並行性を容易に実現し、GOMAXPROCS設定を通じて並列性までも自然に活用します。
一般的に利用率の高いJavaのマルチスレッド (Multi thread) は、並列性の代表的な概念です。
ゴルーチンはなぜ優れているのか?
軽量である (lightweight)
生成コストが他の言語と比較して非常に低いです。ここで、なぜGo言語では少なく使用されるのかという疑問が生じますが、生成位置がGoランタイム内部で管理されているためです。なぜなら、上記の軽量論理スレッドであるためです。OSスレッド単位よりも小さく、初期スタックは2KB程度のサイズを必要とし、ユーザーの実装に応じてスタックを追加して動的に可変するためです。
スタック単位で管理されるため、生成、削除が非常に高速かつ低コストで処理可能であり、数百万個のゴルーチンを稼働させても負担にならない処理が可能です。これにより、GoroutineはランタイムスケジューラのおかげでOSカーネルの介入を最小限に抑えることができます。
性能が良い (performance)
まず、Goroutineは上記の通りOSカーネルの介入が少なく、ユーザーレベル (User-Level) でコンテキストスイッチングを行う際にOSスレッド単位よりもコストが低く、迅速にタスクを切り替えることができます。
他にもM:Nモデルを利用してOSスレッドに割り当てて管理します。OSスレッドプールを作成することで、多くのスレッドを必要とせず、少ないスレッドでも処理が可能です。例えば、システムコールのような待機状態に陥った場合、GoランタイムはOSスレッドで他のゴルーチンを実行し、OSスレッドは休むことなく効率的にCPUを活用して高速な処理を可能にします。
これにより、Go言語は特にI/O処理において他の言語に比べて高い性能を発揮できます。
簡潔である (concise)
並行性が必要な場合、goキーワード一つで関数を簡単に処理できる点も大きな利点です。
Mutex、Semaphoreなど複雑なLockを利用する必要があり、Lockを利用すると必然的に考慮しなければならないデッドロック (DeadLock) 状態を考慮せざるを得ず、開発以前の設計段階から複雑な段階が必要になります。
Goroutineは「メモリを共有して通信するのではなく、通信してメモリを共有せよ」という哲学に基づき、チャネル (Channel)を通じたデータ転送を推奨しており、SELECTはチャネル (Channel) と結合して、データが準備されたチャネルから処理できる機能までサポートしています。また、sync.WaitGroupを利用すれば、複数のゴルーチンがすべて終了するまで簡単に待つことができ、タスクの流れを容易に管理できます。これらのツールのおかげで、スレッド間のデータ競合問題を防止し、より安全に並行処理が可能です。
また、コンテキスト (context) を利用して、ユーザーレベル (User-Level) でライフサイクル、キャンセル、タイムアウト、デッドライン、リクエスト範囲を制御できるため、ある程度の安定性を保証できます。
Goroutineの並列作業 (GOMAXPROCS)
goroutineの並行性が優れている点を述べましたが、並列はサポートしていないのかという疑問を抱かれるかもしれません。最近のCPUのコア数は過去とは異なり二桁を超え、家庭用PCもコア数が少なくありません。
しかし、Goroutineは並列作業も実行します。それがGOMAXPROCSです。
GOMAXPROCSを設定しない場合、バージョンによって設定が異なります。
1.5以前: デフォルト値1、1以上必要な場合は
runtime.GOMAXPOCS(runtime.NumCPU())のような方法での設定が必須1.5 ~ 1.24: 利用可能なすべての論理コア数に変更されました。この時点から、開発者が特に制約を必要としない限り、設定する必要はありません。
1.25: コンテナ環境で有名な言語らしく、Linux上のcGroupを確認し、コンテナに設定された
CPU制限を確認します。すると、論理コア数が10個で、CPU制限値が5の場合、
GOMAXPROCSはより低い数である5に設定されます。
1.25の修正は非常に大きな修正点を含んでいます。それは、コンテナ環境での言語活用度が向上したためです。これにより、不要なスレッド生成とコンテキストスイッチングを減らし、CPUスロットリング (throttling) を防ぐことができるようになりました。
1package main
2
3import (
4 "fmt"
5 "math/rand"
6 "runtime"
7 "time"
8)
9
10func exe(name int, wg *sync.WaitGroup) {
11 defer wg.Done()
12
13 fmt.Printf("Goroutine %d: 開始\n", name)
14 time.Sleep(10 * time.Millisecond) // 作業シミュレーションのための遅延
15 fmt.Printf("Goroutine %d: 終了\n", name)
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // CPUコアを2つだけ使用
20 wg := sync.WaitGroup();
21 goroutineCount := 10
22 wg.Add(goroutineCount)
23
24 for i := 0; i < goroutineCount; i++ {
25 go exe(i, &wg)
26 }
27
28 fmt.Println("すべてのgoroutineが終了するまで待機します...")
29 wg.Wait()
30 fmt.Println("すべての作業が完了しました。")
31
32}
33
Goroutineのスケジューラ (M:Nモデル)
前の内容であるM:Nモデルを利用してOSスレッドに割り当てて管理しますの部分について、もう少し具体的に掘り下げると、goroutine GMPモデルがあります。
- G (Goroutine): Goで実行される最小の作業単位
- M (Machine): OSスレッド (実際の作業場所)
- P (Processor): Goランタイムが管理する論理的なプロセス
です。Pは追加的にローカル実行キュー (Local Run Queue) を持ち、割り当てられたGをMに配分するスケジューラの役割をします。簡単にgoroutineは
GMPの動作過程は以下の通りです
- G (Goroutine) が生成されると、P (Processor) のローカル実行キューに割り当てられます。
- P (Processor) はローカル実行キュー内のG (Goroutine) をM (Machine) に割り当てます。
- M (Machine) はG (Goroutine) の状態であるblock、complete、preemptedを返します。
- Work-Stealing (作業の盗み取り): もしPのローカル実行キューが空になった場合、他のPはグローバルキューを確認します。そこにもG (Goroutine) がない場合、他のローカルP (Processor) の作業を盗み取り、すべてのMが休むことなく動作するようにします。
- システムコール処理 (Blocking): G (Goroutine) が実行中にBlockが発生した場合、M (Machine) は待機状態になりますが、このときP (Processor) はBlockになったM (Machine) と分離し、別のM (Machine) と結合して次のG (Goroutine) を実行します。この際、I/O作業中の待機時間においてもCPUの無駄がありません。
- 一つのG (Goroutine) が長く先占 (preempted) する場合、他のG (Goroutine) に実行機会を与えます。
Go言語はGC (Garbage Collector) もGoroutine上で実行され、アプリケーションの実行を最小限に中断させながら (STW) 並列的にメモリを整理できるため、システムリソースを効率的に使用します。
最後に、Go言語は言語の強力な利点の一つであり、他にも多くの利点がありますので、多くの開発者の方々がGo言語を楽しんでいただければ幸いです。
ありがとうございます。