web-dev-qa-db-ja.com

ゴルーチンを理解する

Goの並行性を理解しようとしています。特に、私はこのスレッドセーフでないプログラムを作成しました。

package main

import "fmt"

var x = 1

func inc_x() { //test
  for {
    x += 1
  }
}

func main() {
  go inc_x()
  for {
    fmt.Println(x)
  }
}

xで競合状態を防ぐためにチャネルを使用する必要があることを認識していますが、それはここでは重要ではありません。プログラムは1を出力し、その後、(何も出力せずに)永久にループしているように見えます。競合状態が原因で、数のリストが無限に出力され、一部がスキップされて他の数が繰り返されることが予想されます(さらに悪いことに、inc_xで更新されている間に数値が出力されます)。

私の質問は:なぜプログラムは1行しか印刷しないのですか?

明確にするために、このおもちゃの例では意図的にチャネルを使用していません。

41
user793587

Goの goroutines について覚えておくべきことがいくつかあります:

  1. これらは、JavaまたはC++スレッドの意味でのスレッドではありません
    • goroutinesは greenlets のようなものです
  2. Goランタイムは、システムスレッド全体でゴルーチンを多重化します。
    • システムスレッドの数は環境変数GOMAXPROCSによって制御され、現在のところデフォルトは1だと思います。これは将来変更される可能性があります
  3. ゴルーチンが現在のスレッドに戻る方法は、いくつかの異なる構成によって制御されます[.____]。
    • selectステートメント はスレッドに制御を戻すことができます
    • channel で送信すると、スレッドに制御を戻すことができます
    • IO操作を実行すると、制御をスレッドに戻すことができます
    • runtime.Gosched() 明示的に制御をスレッドに戻します

表示されている動作は、メイン関数がスレッドに戻ることはなく、代わりにビジーループに関与しているためです。また、スレッドが1つしかないため、メインループを実行する場所がありません。

38
Jeremy Wall

this および this によると、一部の呼び出しは、CPUにバインドされたGoroutineの実行中に呼び出すことができません(Goroutineがスケジューラーに譲らない場合)。これにより、他のGoroutineがメインスレッドをブロックする必要がある場合にハングする可能性があります(write()によって使用されるfmt.Println() syscallの場合など)

私が見つけた解決策は、次のように、CPUバウンドスレッドで runtime.Gosched() を呼び出してスケジューラーに戻ることでした。

_package main

import (
  "fmt"
  "runtime"
)

var x = 1

func inc_x() {
  for {
    x += 1
    runtime.Gosched()
  }
}

func main() {
  go inc_x()
  for {
    fmt.Println(x)
  }
}
_

Goroutineで実行している操作は1つだけなので、runtime.Gosched()は頻繁にvery呼び出されます。 initでruntime.GOMAXPROCS(2)を呼び出すと、桁違いに高速になりますが、数値をインクリメントするよりも複雑な処理(配列、構造体、マップなどの処理など)を行う場合は、スレッドセーフではありません。 。

その場合のベストプラクティスは、チャネルを使用してリソースへの共有アクセスを管理することです。

更新:Go 1.2以降、 インライン化されていない関数呼び出しでスケジューラーを呼び出すことができます。

17
lunixbochs

それは2つの相互作用です。 1つはデフォルトで、Goは単一のコアのみを使用し、2つはGoが協調してゴルーチンをスケジュールする必要があります。関数inc_xは生成されないため、使用されている単一のコアを独占します。これらの条件のいずれかを緩和すると、期待する出力が得られます。

「コア」と言うのは少し光沢があります。 Goは実際にはバックグラウンドで複数のコアを使用する場合がありますが、GOMAXPROCSと呼ばれる変数を使用して、システム以外のタスクを実行しているゴルーチンをスケジュールするスレッドの数を決定します。 [〜#〜] faq [〜#〜] および Effective Go で説明されているように、デフォルトは1ですが、環境変数またはランタイムを使用してより高く設定できます。関数。これにより、期待どおりの出力が得られる可能性がありますが、プロセッサに複数のコアがある場合に限ります。

コアとGOMAXPROCSとは別に、ランタイムでゴルーチンスケジューラーにその仕事をする機会を与えることができます。スケジューラーは実行中のゴルーチンをプリエンプションできませんが、ランタイムに戻ってIO、time.Sleep()、runtime.Gosched()などのサービスを要求するのを待つ必要があります。 inc_xにこのようなものを追加すると、期待される出力が生成されます。 main()を実行しているゴルーチンは、すでにfmt.Printlnを使用してサービスを要求しているため、2つのゴルーチンが定期的にランタイムに譲り、ある種の公平なスケジューリングを行うことができます。

7
Sonia

確かではありませんが、私はthink _inc_x_がCPUを占有していると思います。 IOがないため、制御が解放されません。

私はそれを解決する2つのことを見つけました。 1つは、プログラムの開始時にruntime.GOMAXPROCS(2)を呼び出すことでしたが、現在は2つのスレッドがgoroutingを提供しているため、これは機能します。もう1つは、xをインクリメントした後にtime.Sleep(1)を挿入することです。

2
lazy1