GoSuda

ゴルーチンの基本

By hamori
views ...

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キーワード一つで関数を簡単に処理できる点も大きな利点です。

MutexSemaphoreなど複雑なLockを利用する必要があり、Lockを利用すると必然的に考慮しなければならないデッドロック (DeadLock) 状態を考慮せざるを得ず、開発以前の設計段階から複雑な段階が必要になります。

Goroutineは「メモリを共有して通信するのではなく、通信してメモリを共有せよ」という哲学に基づき、チャネル (Channel)を通じたデータ転送を推奨しており、SELECTはチャネル (Channel) と結合して、データが準備されたチャネルから処理できる機能までサポートしています。また、sync.WaitGroupを利用すれば、複数のゴルーチンがすべて終了するまで簡単に待つことができ、タスクの流れを容易に管理できます。これらのツールのおかげで、スレッド間のデータ競合問題を防止し、より安全に並行処理が可能です。

また、コンテキスト (context) を利用して、ユーザーレベル (User-Level) でライフサイクル、キャンセル、タイムアウト、デッドライン、リクエスト範囲を制御できるため、ある程度の安定性を保証できます。

Goroutineの並列作業 (GOMAXPROCS)

goroutineの並行性が優れている点を述べましたが、並列はサポートしていないのかという疑問を抱かれるかもしれません。最近のCPUのコア数は過去とは異なり二桁を超え、家庭用PCもコア数が少なくありません。

しかし、Goroutineは並列作業も実行します。それがGOMAXPROCSです。

GOMAXPROCSを設定しない場合、バージョンによって設定が異なります。

  1. 1.5以前: デフォルト値1、1以上必要な場合はruntime.GOMAXPOCS(runtime.NumCPU())のような方法での設定が必須

  2. 1.5 ~ 1.24: 利用可能なすべての論理コア数に変更されました。この時点から、開発者が特に制約を必要としない限り、設定する必要はありません。

  3. 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の動作過程は以下の通りです

  1. G (Goroutine) が生成されると、P (Processor) のローカル実行キューに割り当てられます。
  2. P (Processor) はローカル実行キュー内のG (Goroutine) をM (Machine) に割り当てます。
  3. M (Machine) はG (Goroutine) の状態であるblock、complete、preemptedを返します。
  4. Work-Stealing (作業の盗み取り): もしPのローカル実行キューが空になった場合、他のPはグローバルキューを確認します。そこにもG (Goroutine) がない場合、他のローカルP (Processor) の作業を盗み取り、すべてのMが休むことなく動作するようにします。
  5. システムコール処理 (Blocking): G (Goroutine) が実行中にBlockが発生した場合、M (Machine) は待機状態になりますが、このときP (Processor) はBlockになったM (Machine) と分離し、別のM (Machine) と結合して次のG (Goroutine) を実行します。この際、I/O作業中の待機時間においてもCPUの無駄がありません。
  6. 一つのG (Goroutine) が長く先占 (preempted) する場合、他のG (Goroutine) に実行機会を与えます。

Go言語はGC (Garbage Collector) もGoroutine上で実行され、アプリケーションの実行を最小限に中断させながら (STW) 並列的にメモリを整理できるため、システムリソースを効率的に使用します。

最後に、Go言語は言語の強力な利点の一つであり、他にも多くの利点がありますので、多くの開発者の方々がGo言語を楽しんでいただければ幸いです。

ありがとうございます。