1. ホーム
  2. concurrency

[解決済み] runtime.Goschedは具体的に何をするのですか?

2023-01-13 03:03:02

質問

Tour of Go ウェブサイトの go 1.5 リリース以前のバージョン には、次のようなコードの断片があります。

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

出力はこのようになります。

hello
world
hello
world
hello
world
hello
world
hello

気になるのは runtime.Gosched() が削除されると、プログラムはもはや "world" を表示しないことです。

hello
hello
hello
hello
hello

なぜそうなのでしょうか?どのようにして runtime.Gosched() は実行にどのような影響を与えるのでしょうか?

どのように解決するのですか?

注意してください。

Go 1.5 では、GOMAXPROCS はハードウェアのコア数に設定されます。 golang.org/doc/go1.5#runtime 1.5以前のオリジナルの回答は以下の通りです。


環境変数GOMAXPROCSを指定せずにGoプログラムを実行すると、GoゴルーチンはOSのシングルスレッドで実行されるようにスケジュールされます。しかし、プログラムをマルチスレッドに見せるために(そのためにゴルーチンがあるのですね)、Goスケジューラは時々実行コンテキストを切り替えて、各ゴルーチンが自分の仕事をできるようにしなければなりません。

GOMAXPROCS変数が指定されていない場合、Goランタイムは1つのスレッドしか使用できないので、goroutineが計算やIO(これはプレーンC関数にマッピングされています)のような従来の仕事を実行している間は、実行コンテキストを切り替えることはできません。コンテキストを切り替えられるのは、Goの並行処理プリミティブが使われたときだけです。例えば、複数のチャンをオンにしたとき、あるいは(あなたの場合)スケジューラに明示的にコンテキストを切り替えるよう指示したときです。 runtime.Gosched はそのためのものです。

つまり、あるゴルーチンでの実行コンテキストが Gosched の呼び出しに達すると、スケジューラは実行を別のゴルーチンに切り替えるように指示します。あなたの場合、main(プログラムの「メイン」スレッドを表す)とadditionalの2つのゴルーチンがあり、1つはあなたが go say . もし Gosched を削除すると、実行コンテキストは最初のゴルーチンから2番目のゴルーチンに 決して移行せず、したがって、あなたにとって「世界」はありません。このとき Gosched が存在する場合、スケジューラは各ループの反復実行を最初のゴルーチンから2番目のゴルーチンに転送し、その逆も行われます。

参考までに、これは「協調型マルチタスク」と呼ばれ、ゴルーチンは明示的に他のゴルーチンに制御を委ねる必要があります。実行スレッドは制御の転送に関係なく、代わりにスケジューラーが実行コンテキストを透過的に切り替えます。協調的なアプローチは「グリーンスレッド」、つまり OS のスレッドに 1 対 1 で対応しない論理的な同時実行コルーチンを実装するために頻繁に使用されており、Go ランタイムとそのゴルーチンはこの方法で実装されています。

更新

GOMAXPROCS 環境変数について触れましたが、それが何であるかは説明していませんでした。これを修正する時が来ました。

この変数が正の数に設定されている場合 N に設定されると、Go ランタイムは最大で N ネイティブスレッドを作成し、その上で全てのグリーンスレッドがスケジュールされます。ネイティブスレッドとは、オペレーティングシステムによって作成されるスレッドの一種です(Windowsスレッド、pthreadsなど)。つまり、もし N が 1 より大きい場合、ゴルーチンは異なるネイティブスレッドで実行されるようにスケジュールされ、結果として並列に実行される可能性があります (少なくともコンピュータの性能次第では。システムがマルチコアプロセッサに基づいている場合、これらのスレッドは本当に並列である可能性があります。)

GOMAXPROCS 変数の設定は runtime.GOMAXPROCS() 関数を使って設定することができます。このような場合は、現在の main :

func main() {
    runtime.GOMAXPROCS(2)
    go say("world")
    say("hello")
}

この場合、興味深い結果を観察することができます。例えば、'hello' と 'world' の行が不規則に挿入されて印刷されることがあり得ます。

hello
hello
world
hello
world
world
...

これはゴルーチンがOSスレッドに分割してスケジューリングされている場合に起こりえます。これは実際、プリエンプティブなマルチタスク(あるいはマルチコアシステムの場合の並列処理)がどのように機能するかということです。スレッドは並列であり、それらの組み合わせの出力は不定です。ちなみに Gosched の呼び出しは、GOMAXPROCS が 1 よりも大きい場合は効果がないようです。

を使用してプログラムを何度か実行したところ、次のような結果が得られました。 runtime.GOMAXPROCS を呼び出すと、次のようになります。

hyperplex /tmp % go run test.go
hello
hello
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
hello
hello
hello
hello
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world

ほら、出力がきれいなときとそうでないときがあるでしょ。非決定論的な行動です :)

もう一つの更新

新しいバージョンのGoコンパイラでは、Goランタイムは同時実行プリミティブの使用時だけでなく、OSのシステムコールでもゴルーチンを強制的に降伏させるようになったようです。これは、IO 関数の呼び出しでもゴルーチン間で実行コンテキストが切り替わることを意味します。その結果、最近のGoコンパイラでは、GOMAXPROCSが未設定または1に設定されている場合でも、不定な動作を観察することが可能になっています。